RIC-1059: dms_cli to use flask-restx
[ric-plt/appmgr.git] / scripts / appmgrcli
1 #!/usr/bin/perl -w
2 #
3 # Copyright (c) 2019 AT&T Intellectual Property.
4 # Copyright (c) 2019 Nokia.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #     http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 #
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 #############################
20 # Simple cli for xapp manager
21 #
22 # In addition to standard shell tools, requires basic Perl installation
23 # (Ubuntu package "perl-base", installed by default), packages "curl" and
24 # "yajl-tools" (the second provides json_reformat on Ubuntu; on Red Hat-style
25 # distributions install "yajl" instead).
26 #
27 use strict;
28 use Getopt::Long;
29 use Fcntl;
30
31 my $myname="appmgrcli";
32
33 sub usage {
34     print <<"EOF1";
35 usage: $myname [-h host] [-p port] [-v] [-c curlprog] command params...
36 - command is deploy, undeploy, status, subscriptions, health, config, help
37 - (abbreviations dep, undep, stat, subs, heal allowed)
38 - Parameters of the commands that may have parameters:
39 -- deploy: name of the xapp to deploy
40 ---- Deployment parameters come from the Help chart but the following can be
41 ---- overridden by option with the same name (for example --configName=cname):
42 ----- helmVersion ReleaseName namespace podHost (host of pods).
43 -- undeploy: name of the xapp to undeploy
44 -- status:
45 ---- No parameters: Lists information about all deployed xapps
46 ---- xapp name as parameter: Prints information about the given xapp
47 ---- xapp name and instance: Lists information about the given instance only
48 -- subscriptions is followed by sub-command list, add, delete, or modify
49 --- (abbreviations del and mod for delete and modify are allowed):
50 ---- list without parameters lists all subscriptions
51 ---- list with subscription id prints that subscription
52 ---- add URL eventType maxRetry retryTimer
53 ------- URL is the URL to notify
54 ------- eventType one of created,deleted,all
55 ------- maxRetry and retryTimer are positive decimal numbers
56 ---- modify id URL eventType maxRetry retryTimer
57 ------- id is the subscription id (find out with the list command)
58 --------the rest of the parameters are like in add
59 ---- delete id
60 ------- id is the subscription id to delete (find out with the list command)
61 -- config is followed by sub-command list, add, delete, or modify
62 --- (abbreviations del and mod for delete and modify are allowed):
63 ---- list (no pars)
64 ------ lists the configuration of all xapps
65 ---- add jsonfile
66 ------ Creates xapp configuration. the jsonfile must contain all data (see API)
67 ---- add name configName namespace configSchemaFile configDataFile
68 ------ Creates xapp configuration, but unlike in the 1-parameter form,
69 ------ Xapp name, config map name and namespace are separate parameters.
70 ------ The other data come from JSON files.
71 ---- modify
72 ------ Modifies existing configuration. Same parameters (1 or 5) as in add.
73 ---- delete name configName namespace
74 ------ Deletes the configuration identified by the parameters.
75 --------------------------------------------------------------
76 - Default values for host and port can be set in environment
77 - variables APPMGR_HOST and APPMGR_PORT
78 - Option -v sets verbose mode.
79 - Option -c overrides the used curl program name ("curl" by default).
80 - Exit code is 0 for success, 1 for any kind of failure.
81 EOF1
82     exit 0;
83 }
84
85 sub helphint {
86     print "run $myname help (or --help) for instructions\n";
87 }
88
89 # Defaults
90
91 my $host="localhost";
92 my $port=8080;
93 my $verbose=0;
94 my $showhelp = 0;
95
96 # API URLs
97
98 my $base="/ric/v1";
99 my $base_xapps="$base/xapps";
100 my $base_health="$base/health";
101 my $base_subs="$base/subscriptions";
102 my $base_config="$base/config";
103
104 # Check for environment override
105 if (exists $ENV{"APPMGR_HOST"}) {
106    $host=$ENV{"APPMGR_HOST"};
107 }
108 if (exists $ENV{"APPMGR_PORT"}) {
109     $port=$ENV{"APPMGR_PORT"};
110 }
111
112 # Overrides for some deploy parameters
113
114 my $configName = "";
115 my $namespace = "ricxapp";
116 # Check for environment override
117 if (exists $ENV{"XAPP_NAMESPACE"}) {
118    $namespace=$ENV{"XAPP_NAMESPACE"};
119 }
120 my $releaseName = "";
121 my $helmVersion = "0.0.1";
122 my $overrideFile = "";
123 my $podHost = "";
124
125 # The curl command can be overridden for testing with a dummy.
126
127 my $curl = "curl";
128
129 Getopt::Long::Configure("no_auto_abbrev", "permute");
130 if (! GetOptions("h=s" => \$host,
131                  "p=i" => \$port,
132                  "c=s" => \$curl,
133                  "ConfigName=s" => \$configName,
134                  "Namespace=s" => \$namespace,
135                  "ReleaseName=s" => \$releaseName,
136                  "HelmVersion=s" => \$helmVersion,
137                  "OverrideFile=s" => \$overrideFile,
138                  "podHost=s" => \$podHost,
139                  "help" => \$showhelp,
140                  "v" => \$verbose)) {
141     print "$myname: Error in options\n";
142     helphint();
143     exit 1;
144 }
145
146 if ($showhelp) {
147     usage();
148     exit 0;
149 }
150
151 if ($verbose) {
152     print "host = $host\n";
153     print "port = $port\n";
154     print "ConfigName = $configName\n";
155     print "Namespace = $namespace\n";
156     print "ReleaseName = $releaseName\n";
157     print "HelmVersion = $helmVersion\n";
158      print "OverrideFile = $overrideFile\n";
159     print "podHost = $podHost\n";
160     for (my $idx = 0; $idx <= $#ARGV; ++$idx) {
161         print "\$ARGV[$idx] = $ARGV[$idx]\n";
162     }
163 }
164
165 # Verify command and call handler function
166
167 my %commands = (
168     "deploy" => \&do_deploy,
169     "dep" => \&do_deploy,
170     "undeploy" => \&do_undeploy,
171     "undep" => \&do_undeploy,
172     "status" => \&do_status,
173     "stat" =>  \&do_status,
174     "subscriptions" => \&do_subscriptions,
175     "subs" =>  \&do_subscriptions,
176     "health" => \&do_health,
177     "heal" => \&do_health,
178     "config" => \&do_config,
179     "help" => \&usage
180 );
181
182 if ($#ARGV < 0) {
183     print "$myname: Missing command\n";
184     helphint();
185     exit 1;
186 }
187
188 # Variable status used for the return value of the whole script.
189 my $status = 0;
190
191 my $command = $ARGV[0];
192 shift;
193 if (exists $commands{$command}) {
194     # Call the handler function with the rest of the command line
195     $commands{$command}(@ARGV);
196     exit $status; # Default exit. A handler can exit also if more convenient
197 }
198 print "$myname: Unrecognised command $command\n";
199 helphint();
200 exit 1;
201
202 my $errfile;
203 my $resultfile;
204
205
206 sub make_temp_name($) {
207     my $tmpsuffix = "${$}${^T}";
208     return "$_[0].$tmpsuffix";
209 }
210
211 sub make_temps {
212     $errfile = make_temp_name("/tmp/appmgr_e");
213     $resultfile = make_temp_name("/tmp/appmgr_r");
214 }
215
216 sub remove_temps {
217     unlink ($errfile, $resultfile);
218 }
219
220 sub print_file($$) {
221     my $outputhandle = $_[0];
222     my $filename = $_[1];
223     my $buffer;
224     my $inhandle;
225     if (!open($inhandle, "<", $filename)) {
226         print $outputhandle "$myname print_file: cannot open $filename: $!\n";
227         return;
228     }
229     while (read($inhandle, $buffer, 4000) > 0) {
230         print $outputhandle $buffer;
231     }
232     close($inhandle);
233 }
234
235 # The HTTP protocol result code, filled in by rest().
236
237 my $http_code = "";
238
239 # Helper: Given a curl output file, extract the number from ##code line.
240 # return ERROR if file cannot be opened, or "" if no code found.
241
242 sub find_http_code($) {
243     my ($fh, $line, $code);
244     open($fh, "<", $_[0]) or return "ERROR";
245     while ($line = <$fh>) {
246         if ($line =~ /^##([0-9]+)/) {
247             return $1;
248         }
249     }
250     return "";
251 }
252
253 # Helper for command execution:
254 # Do a rest call with "curl": $1 = method, $2 = path (without host and port
255 # which come from variables), $3 data to POST if needed
256 # returns true (1) if OK, and any returned data is in $resultfile
257 # else 0, and error message from curl is in $errfile, which is printed
258 # before returning the 0.
259 #
260 # On curl options: --silent --show-error disables progress bar, but allows
261 # error messages. --connect-timeout 20 limits waiting for connection to
262 # 20 seconds. In practice connection will succeed almost immediately,
263 # or in the case of wrong address not at all.
264 # To get the http code, using -w with format. The result comes at the end
265 # of the output, so "decorating" it for easier filtering.
266 # The code is put to global $http_code.
267 #
268 sub rest($$_) {
269     my $method = $_[0];
270     my $path = $_[1];
271     my $data = $_[2] || "";
272     my $retval = 1;
273     my $http_status_file = make_temp_name("/tmp/appmgr_h");
274
275     # This redirects stderr (fd 2) to $errfile, but saving normal stderr
276     # so that if can be restored.
277     open(OLDERR, ">&", \*STDERR) or die "Can't dup STDERR: $!";
278     open(ERRFILE, ">", $errfile) or die "open errorfile failed";
279     open(STDERR, ">&", \*ERRFILE) or die "Can't dup ERRFILE: $!";
280
281     # This redirects stdout (fd 1) to $http_status_file, but saving original
282     # so that if can be restored.
283     open(OLDSTDOUT, ">&", \*STDOUT) or die "Can't dup STDOUT: $!";
284     open(HTTP_STATUS_FILE, ">", $http_status_file) or die "open http status file failed";
285     open(STDOUT, ">&", \*HTTP_STATUS_FILE) or die "Can't dup HTTP_STATUS_FILE: $!";
286
287     my @args = ($curl, "--silent", "--show-error", "--connect-timeout", "20",
288                 "--header", "Content-Type: application/json", "-X", $method,
289                 "-o", $resultfile, "-w", '\n##%{http_code}\n',
290                 "http://${host}:${port}${path}");
291     if ($data ne "") {
292         push(@args, "--data");
293         push(@args, $data);
294     }
295     if ($verbose) {
296         print OLDSTDOUT "Running: " . join(" ", @args) . "\n";
297     }
298     if (system(@args) == -1) {
299         print OLDSTDOUT "$myname: failed to execute @args\n";
300         $retval = 0;
301     }
302     elsif ($? & 127) {
303          printf OLDSTDOUT "$myname: child died with signal %d, %s coredump\n",
304              ($? & 127),  ($? & 128) ? 'with' : 'without';
305          $retval = 0;
306     }
307     else {
308         my $curl_exit_code = $? >> 8;
309         if ($curl_exit_code == 0) {
310             seek HTTP_STATUS_FILE, 0, 0; # Ensures flushing
311             $http_code = find_http_code($http_status_file);
312             if ($http_code eq "ERROR") {
313                 print OLDSTDOUT "$myname: failed to open temp file $http_status_file\n";
314                 $retval = 0;
315             }
316             elsif ($http_code eq "") {
317                 print OLDSTDOUT "$myname: curl failed to provide HTTP code\n";
318                 $retval = 0;
319             }
320             else {
321                 if ($verbose) {
322                     print OLDSTDOUT "HTTP status code = $http_code\n";
323                 }
324                 $retval = 1; # Interaction OK from REST point of view
325             }
326         }
327         else {
328             print_file(\*OLDSTDOUT, $errfile);
329             $retval = 0;
330         }
331     }
332     open(STDOUT, ">&", \*OLDSTDOUT) or die "Can't dup OLDSTDOUT: $!";
333     open(STDERR, ">&", \*OLDERR) or die "Can't dup OLDERR: $!";
334     unlink($http_status_file);
335     return $retval;
336 }
337
338 # Pretty-print a JSON file to stdout.
339 # (currently uses json_reformat command)
340 # Skips the ##httpcode line we make "curl"
341 # add in order to get access to the HTTP status.
342
343 sub print_json($) {
344     my $filename = $_[0];
345     my ($line, $inhandle, $outhandle);
346     if (!open($inhandle, "<", $filename)) {
347         print "$myname print_json: cannot open $filename: $!\n";
348         return;
349     }
350     if (!open($outhandle, "|json_reformat")) {
351         print "$myname print_json: cannot pipe to json_reformat: $!\n";
352         return;
353     }
354     while ($line = <$inhandle>) {
355         if (! ($line =~ /^##[0-9]+/)) {
356             print $outhandle $line;
357         }
358     }
359     close($outhandle);
360     close($inhandle);
361 }
362
363 # Append an entry like ","name":"value" to the first parameter, if "name"
364 # names a variable with non-empty value.
365 # Else returns the unmodified first parameter.
366
367 sub append_option($$) {
368     my $result = $_[0];
369     my $var = $_[1];
370     my $val = eval("\$$var");
371     if ($val ne "") {
372         $result = "$result,\"$var\":\"$val\"";
373     }
374     return $result;
375 }
376
377 # Command handlers
378 # Assumes the API currently implemented.
379 # Functions for each command below
380
381 # Deploy:
382 # The deploy command has one mandatory parameter "name" in the API,
383 # and several optional ones. Used mainly internally for testing, because
384 # they all override Helm chart values:
385 # "helmVersion": Helm chart version to be used
386 # "releaseName": The releas name of xApp visible in K8s
387 # "namespace":  Name of the namespace to which xApp is deployed.
388 # "overrideFile":  The file content used to override values.yaml file
389 # this host from the host the xapp manager is running in, we use the term
390 # and variable name "podHost" here.
391 # The options come from options (see GetOptions() call).
392
393 sub do_deploy(@) {
394     my $name = $_[0] || "";
395     if ($name ne "") {
396         my $data = "{\"XappName\":\"$name\"";
397         $data = append_option($data, "helmVersion");
398         $data = append_option($data, "releaseName");
399         $data = append_option($data, "namespace");
400         $data = append_option($data, "overrideFile");
401         $data = $data . "}";
402         make_temps();
403         if (rest("POST", $base_xapps, $data)) {
404             if ($http_code eq "201") {
405                 print_json $resultfile;
406                 $status = 0;
407             }
408             else {
409                 my $error;
410                 if ($http_code eq "400") {
411                     $error = "INVALID PARAMETERS SUPPLIED";
412                 }
413                 elsif ($http_code eq "500") {
414                     $error = "INTERNAL ERROR";
415                 }
416                 else {
417                     $error = "UNKNOWN STATUS $http_code";
418                 }
419                 print "$error\n";
420                 $status = 1;
421             }
422         }
423         else {
424             $status=1;
425         }
426         remove_temps();
427     }
428     else {
429         print "$myname: Error: expected the name of xapp to deploy\n";
430         $status = 1;
431     }
432 }
433
434 sub do_undeploy(@) {
435     my $name = $_[0] || "";
436     my $urlpath = $base_xapps;
437     if ($name ne "") {
438         make_temps();
439         $urlpath = "$urlpath/$name";
440         if (rest("DELETE", $urlpath)) {
441             if ($http_code eq "204") {
442                 print "SUCCESSFUL DELETION\n";
443                 $status = 0;
444             }
445             else {
446                 my $error;
447                 if ($http_code eq "400") {
448                     $error = "INVALID XAPP NAME SUPPLIED";
449                 }
450                 elsif ($http_code eq "500") {
451                     $error = "INTERNAL ERROR";
452                 }
453                 else {
454                     $error = "UNKNOWN STATUS $http_code";
455                 }
456                 print "$error\n";
457                 $status = 1;
458             }
459         }
460         else {
461             $status = 1;
462         }
463         remove_temps();
464     }
465     else {
466         print "$myname: Error: expected the name of xapp to undeploy\n";
467         $status = 1;
468     }
469 }
470
471 sub do_status(@) {
472     my $name = $_[0] || "";
473     my $instance = $_[1] || "";
474     my $urlpath = $base_xapps;
475
476     if ($name ne "") {
477         $urlpath = "$urlpath/$name";
478     }
479     if ($instance ne "") {
480         $urlpath = "$urlpath/instances/$instance"
481     }
482     make_temps();
483     if (rest("GET", $urlpath)) {
484         if ($http_code eq "200") {
485             print_json $resultfile;
486             $status = 0;
487         }
488         else {
489             my $error;
490             if ($http_code eq "400") {
491                 $error = "INVALID XAPP NAME SUPPLIED";
492             }
493             if ($http_code eq "404") {
494                 $error = "XAPP NOT FOUND";
495             }
496             elsif ($http_code eq "500") {
497                 $error = "INTERNAL ERROR";
498             }
499             else {
500                 $error = "UNKNOWN STATUS $http_code";
501             }
502             print "$error\n";
503             $status = 1;
504         }
505     }
506     else {
507         $status = 1;
508     }
509     remove_temps();
510 }
511
512 # Helpers for subscription:
513 # Validate the subscription data that follows a subscription add or modify
514 # subcommand. $1=URL, $2=eventType, $3=maxRetries, $4=retryTimer
515 # URL must look like URL, event type must be one of created deleted all,
516 # maxRetries and retryTimer must be non-negative numbers.
517 # If errors, returns false (0) and prints errors, else returns 1.
518 #
519 sub validate_subscription(@) {
520     # Using the API parameter names
521     my $targetUrl = $_[0] || "";
522     my $eventType = $_[1] || "";
523     my $maxRetries = $_[2] || "";
524     my $retryTimer = $_[3] || "";
525     my $retval = 1;
526
527     if (! ($targetUrl =~ /^http:\/\/.*/ or $targetUrl =~ /^https:\/\/.*/)) {
528         print "$myname: bad URL $targetUrl\n";
529         $retval = 0;
530     }
531     if ($eventType ne "created" and $eventType ne "deleted" and
532         $eventType ne "all") {
533         print "$myname: unrecognized event $eventType\n";
534         $retval = 0;
535     }
536     if (! ($maxRetries =~ /^[0-9]+$/)) {
537         print "$myname: invalid maximum retries count $maxRetries\n";
538         $retval = 0;
539     }
540     if (! ($retryTimer =~ /^[0-9]+$/)) {
541         print "$myname: invalid retry time $retryTimer\n";
542         $retval = 0;
543     }
544     return $retval;
545 }
546
547 # Format a subscriptionRequest JSON object
548
549 sub make_subscriptionRequest(@) {
550     my $targetUrl = $_[0];
551     my $eventType = $_[1];
552     my $maxRetries = $_[2];
553     my $retryTimer = $_[3];
554     return "{\"Data\": {\"TargetUrl\":\"$targetUrl\",\"EventType\":\"$eventType\",\"MaxRetries\":$maxRetries,\"RetryTimer\":$retryTimer}}";
555 }
556
557 # Subscriptions:
558 # $1 is sub-command: list, add, delete, modify
559
560 sub do_subscriptions(@) {
561     my $subcommand = $_[0] || "";
562     shift;
563
564     my %subcommands = (
565         "list" => \&do_subscription_list,
566         "add" => \&do_subscription_add,
567         "delete" => \&do_subscription_delete,
568         "del" => \&do_subscription_delete,
569         "modify" => \&do_subscription_modify,
570         "mod" => \&do_subscription_modify
571     );
572     if (exists $subcommands{$subcommand}) {
573         $subcommands{$subcommand}(@_);
574     }
575     else {
576         print "$myname: unrecognized subscriptions subcommand $subcommand\n";
577         helphint();
578         $status=1
579     }
580 }
581
582 # list: With empty parameter, list all, else the parameter is
583 # a subscriptionId
584
585 sub do_subscription_list(@) {
586     my $urlpath=$base_subs;
587     my $subscriptionId = $_[0] || "";
588     if ($subscriptionId ne "") {
589         $urlpath = "$urlpath/$subscriptionId";
590     }
591     make_temps();
592     if (rest("GET", $urlpath)) {
593         if ($http_code eq "200") {
594             print_json $resultfile;
595             $status = 0;
596         }
597         else {
598             my $error;
599             if ($http_code eq "400") {
600                 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
601             }
602             elsif ($http_code eq "404") {
603                 $error = "SUBSCRIPTION $subscriptionId NOT FOUND";
604             }
605             elsif ($http_code eq "500") {
606                 $error = "INTERNAL ERROR";
607             }
608             else {
609                 $error = "UNKNOWN STATUS $http_code";
610             }
611             print "$error\n";
612             $status = 1;
613         }
614     }
615     else {
616         $status=1;
617     }
618     remove_temps();
619 }
620
621 sub do_subscription_add(@) {
622     my $urlpath=$base_subs;
623
624     if (validate_subscription(@_)) {
625         make_temps();
626         if (rest("POST", $urlpath, make_subscriptionRequest(@_))) {
627             if ($http_code eq "201") {
628                 print_json $resultfile;
629                 $status = 0;
630             }
631             else {
632                 my $error;
633                 if ($http_code eq "400") {
634                     $error = "INVALID INPUT";
635                 }
636                 elsif ($http_code eq "500") {
637                     $error = "INTERNAL ERROR";
638                 }
639                 else {
640                     $error = "UNKNOWN STATUS $http_code";
641                 }
642                 print "$error\n";
643                 $status = 1;
644             }
645         }
646         else {
647             $status=1;
648         }
649         remove_temps();
650     }
651     else {
652         $status = 1;
653     }
654 }
655
656 sub do_subscription_delete(@) {
657     my $urlpath=$base_subs;
658     my $subscriptionId = $_[0] || "";
659     if ($subscriptionId ne "") {
660         $urlpath = "$urlpath/$subscriptionId";
661     }
662     else {
663         print "$myname: delete: Subscription id required\n";
664         $status=1;
665         return;
666     }
667     make_temps();
668     if (rest("DELETE", $urlpath)) {
669         if ($http_code eq "204") {
670             print "SUBSCRIPTION $subscriptionId DELETED\n";
671             $status = 0;
672         }
673         else {
674             my $error;
675             if ($http_code eq "400") {
676                 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
677             }
678             elsif ($http_code eq "500") {
679                 $error = "INTERNAL ERROR";
680             }
681             else {
682                 $error = "UNKNOWN STATUS $http_code";
683             }
684             print "$error\n";
685             $status = 1;
686         }
687     }
688     else {
689         $status = 1;
690     }
691     remove_temps();
692 }
693
694 sub do_subscription_modify(@) {
695     my $urlpath=$base_subs;
696     if (defined $_[0]) {
697         $urlpath = "$urlpath/$_[0]";
698     }
699     else {
700         print "$myname: modify: Subscription id required\n";
701         $status=1;
702         return;
703     }
704     shift;
705     if (validate_subscription(@_)) {
706         make_temps();
707         if (rest("PUT", $urlpath, make_subscriptionRequest(@_))) {
708             if ($http_code eq "200") {
709                 print_json $resultfile;
710                 $status = 0;
711             }
712             else {
713                 my $error;
714                 if ($http_code eq "400") {
715                     $error = "INVALID INPUT";
716                 }
717                 elsif ($http_code eq "500") {
718                     $error = "INTERNAL ERROR";
719                 }
720                 else {
721                     $error = "UNKNOWN STATUS $http_code";
722                 }
723                 print "$error\n";
724                 $status = 1;
725             }
726         }
727         else {
728             $status=1;
729         }
730         remove_temps();
731     }
732     else {
733         $status = 1;
734     }
735 }
736
737 sub do_health(@) {
738     my $urlpath=$base_health;
739     my $check = $_[0] || "";
740     # API now defines two types of checks, either of
741     # which must be specified.
742     if ($check ne "alive" and $check ne "ready") {
743         print "$myname: health check type required (alive or ready)\n";
744         $status=1;
745         return;
746     }
747     $urlpath = "$urlpath/$check";
748     make_temps();
749     if (rest("GET", $urlpath)) {
750         my $res;
751         if ($check eq "alive") {
752             # If GET succeeds at all, the xapp manager is alive, no
753             # need to check the HTTP code.
754             $res = "ALIVE";
755         }
756         else {
757             if ($http_code eq "200") {
758                 $res = "READY";
759             }
760             elsif ($http_code eq "503") {
761                 $res = "NOT READY";
762             }
763             elsif ($http_code eq "500") {
764                 $res = "INTERNAL ERROR";
765             }
766             else {
767                 $res = "UNKNOWN STATUS $http_code";
768             }
769         }
770         print "$res\n";
771     }
772     else {
773         $status = 1;
774         print "$myname: health check failed to contact appmgr\n";
775     }
776     remove_temps();
777 }
778
779 sub do_config(@) {
780     my $subcommand = $_[0] || "";
781     shift;
782
783     my %subcommands = (
784         "list" => \&do_config_list,
785         "add" => \&do_config_add,
786         "delete" => \&do_config_delete,
787         "del" => \&do_config_delete,
788         "modify" => \&do_config_modify,
789         "mod" => \&do_config_modify
790     );
791     if (exists $subcommands{$subcommand}) {
792         $subcommands{$subcommand}(@_);
793     }
794     else {
795         print "$myname: unrecognized config subcommand $subcommand\n";
796         helphint();
797         $status=1
798     }
799 }
800
801 sub do_config_list(@) {
802     if (defined $_[0]) {
803         print "$myname: \"config list\" has no parameters\n";
804         $status = 1;
805         return;
806     }
807     make_temps();
808     if (rest("GET", $base_config)) {
809         if ($http_code eq "200") {
810             print_json $resultfile;
811             $status = 0;
812         }
813         else {
814             my $error;
815             if ($http_code eq "500") {
816                 $error = "INTERNAL ERROR";
817             }
818             else {
819                 $error = "UNKNOWN STATUS $http_code";
820             }
821             print "$error\n";
822             $status = 1;
823         }
824     }
825     else {
826         $status=1;
827     }
828     remove_temps();
829 }
830
831 # validate_config() checks configuration commmand line.
832 # "config add" and "config modify" expect either single parameter which
833 # must be a JSON file that contains the whole thing to send (see API),
834 # or 5 parameters, where the first three are
835 # $_[0] = name
836 # $_[1] = configName (name of the configMap)
837 # $_[2] = namespace
838 # Followed by two file names:
839 # $_[3] = file containing configSchema
840 # $_[4] = file containing data for configMap
841 # Giving the last two literally on the command line does not make much sense,
842 # since they are arbitrary JSON data.
843 # On success, returns parameter count (1 or 5), depending on which kind of
844 # command line found.
845 # 0 if errors.
846
847 # Check only the 3 names at the beginning of config add/modify/delete
848 sub validate_config_names(@) {
849     my $retval = 1;
850     # Names in the Kubernetes world consist of lowercase alphanumerics
851     # and - and . as specified in
852     # https://kubernetes.io/docs/concepts/overview/working-with-objects/name
853     for (my $idx = 0; $idx <= 2; ++$idx) {
854         if (! ($_[$idx] =~ /^[a-z][-a-z0-9.]*$/)) {
855             print "$myname: invalid characters in name $_[$idx]\n";
856             $retval = 0;
857         }
858     }
859     return $retval;
860 }
861
862 sub validate_config(@) {
863     my $retval = 1;
864     print "validate_config args @_\n";
865     if ($#_ == 0) {
866         if (! -r $_[0]) {
867             print "$myname: config file $_[0] cannot be read: $!\n";
868             $retval = 0;
869         }
870     }
871     elsif ($#_ == 4) {
872         $retval = 5;
873         if (! validate_config_names(@_)) {
874             $retval = 0;
875         }
876         for (my $idx = 3; $idx <= 4; ++$idx) {
877             if (! -r $_[$idx]) {
878                 print "$myname: cannot read file $_[$idx]\n";
879                 $retval = 0;
880             }
881         }
882     }
883     else {
884         print "$myname: config add: 1 or 5 parameter expected\n";
885         $retval = 0;
886     }
887     return $retval;
888 }
889
890 # Generate JSON for the xAppConfig element (see API).
891
892 sub make_xAppConfigInfo($$$) {
893     return "{\"xAppName\":\"$_[0]\",\"configMapName\":\"$_[1]\",\"namespace\":\"$_[2]\"}";
894 }
895
896 sub make_xAppConfig(@) {
897     my $retval =  "{\"xAppConfigInfo\":" . make_xAppConfigInfo($_[0],$_[1],$_[2]);
898     my $fh;
899     open($fh, "<", $_[3]) or die "failed to open $_[3]";
900     my @obj = <$fh>;
901     close($fh);
902     $retval = $retval . ",\"configSchema\":" . join("", @obj);
903     open($fh, "<", $_[4]) or die "failed to open $_[4]";
904     @obj = <$fh>;
905     close($fh);
906     $retval = $retval . ",\"configMap\":" . join("", @obj) . "}";
907 }
908
909 sub do_config_add(@) {
910     my $paramCount;
911
912     $paramCount = validate_config(@_);
913     if ($paramCount > 0) {
914         my $xAppConfig;
915         if ($paramCount == 1) {
916             $xAppConfig = "\@$_[0]";
917         }
918         else {
919             $xAppConfig = make_xAppConfig(@_);
920         }
921         make_temps();
922         if (rest("POST", $base_config, $xAppConfig)) {
923             if ($http_code eq "201") {
924                 print_json $resultfile;
925                 $status = 0;
926             }
927             elsif ($http_code eq "422") { # Validation failed, details in result
928                 print_json $resultfile;
929                 $status = 1;
930             }
931             else {
932                 my $error;
933                 if ($http_code eq "400") {
934                     $error = "INVALID INPUT";
935                 }
936                 elsif ($http_code eq "500") {
937                     $error = "INTERNAL ERROR";
938                 }
939                 else {
940                     $error = "UNKNOWN STATUS $http_code";
941                 }
942                 print "$error\n";
943                 $status = 1;
944             }
945         }
946         else {
947             $status=1;
948         }
949         remove_temps();
950     }
951     else {
952         $status = 1;
953     }
954 }
955
956 sub do_config_modify(@) {
957     my $paramCount;
958
959     $paramCount = validate_config(@_);
960     if ($paramCount > 0) {
961         my $xAppConfig;
962         if ($paramCount == 1) {
963             $xAppConfig = "\@$_[0]";
964         }
965         else {
966             $xAppConfig = make_xAppConfig(@_);
967         }
968         make_temps();
969         if (rest("PUT", $base_config, $xAppConfig)) {
970             if ($http_code eq "200") {
971                 print_json $resultfile;
972                 $status = 0;
973             }
974             elsif ($http_code eq "422") { # Validation failed, details in result
975                 print_json $resultfile;
976                 $status = 1;
977             }
978             else {
979                 my $error;
980                 if ($http_code eq "400") {
981                     $error = "INVALID INPUT";
982                 }
983                 elsif ($http_code eq "500") {
984                     $error = "INTERNAL ERROR";
985                 }
986                 else {
987                     $error = "UNKNOWN STATUS $http_code";
988                 }
989                 print "$error\n";
990                 $status = 1;
991             }
992         }
993         else {
994             $status=1;
995         }
996         remove_temps();
997     }
998     else {
999         $status = 1;
1000     }
1001 }
1002
1003 # In config delete, allow either 1 parameter naming a file that contains
1004 # a JSON xAppConfigInfo object, or 3 parameters giving the
1005 # components (xAppName, configMapName, namespace), same as
1006 # in add and modify operations.
1007
1008 sub do_config_delete(@) {
1009     my $xAppConfigInfo = "";
1010
1011     if ($#_ != 0 and $#_ != 2) {
1012         print "$myname: wrong number of parameters for config delete\n";
1013         $status = 1;
1014     }
1015     elsif ($#_ == 0) {
1016         if (-r $_[0]) {
1017             $xAppConfigInfo = "\@$_[0]";
1018         }
1019         else {
1020             print "$myname: config file $_[0] cannot be read: $!\n";
1021             $status = 1;
1022         }
1023     }
1024     elsif (($#_ == 2) && validate_config_names(@_)) {
1025         $xAppConfigInfo = make_xAppConfigInfo($_[0],$_[1],$_[2]);
1026     }
1027     else {
1028         print "$myname: bad parameters for config delete\n";
1029         $status = 1;
1030     }
1031     if ($xAppConfigInfo ne "") {
1032         make_temps();
1033         if (rest("DELETE", $base_config, $xAppConfigInfo)) {
1034             if ($http_code eq "204") {
1035                 print "SUCCESFUL DELETION OF CONFIG\n";
1036                 $status = 0;
1037             }
1038             else {
1039                 my $error;
1040                 if ($http_code eq "400") {
1041                     $error = "INVALID PARAMETERS SUPPLIED";
1042                 }
1043                 elsif ($http_code eq "500") {
1044                     $error = "INTERNAL ERROR";
1045                 }
1046                 else {
1047                     $error = "UNKNOWN STATUS $http_code";
1048                 }
1049                 print "$error\n";
1050                 $status = 1;
1051             }
1052         }
1053         else {
1054             $status=1;
1055         }
1056         remove_temps();
1057     }
1058 }