3 # Copyright (c) 2019 AT&T Intellectual Property.
4 # Copyright (c) 2019 Nokia.
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 #############################
20 # Simple cli for xapp manager
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).
31 my $myname="appmgrcli";
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
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
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):
64 ------ lists the configuration of all xapps
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.
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.
86 print "run $myname help (or --help) for instructions\n";
99 my $base_xapps="$base/xapps";
100 my $base_health="$base/health";
101 my $base_subs="$base/subscriptions";
102 my $base_config="$base/config";
104 # Check for environment override
105 if (exists $ENV{"APPMGR_HOST"}) {
106 $host=$ENV{"APPMGR_HOST"};
108 if (exists $ENV{"APPMGR_PORT"}) {
109 $port=$ENV{"APPMGR_PORT"};
112 # Overrides for some deploy parameters
115 my $namespace = "ricxapp";
116 # Check for environment override
117 if (exists $ENV{"XAPP_NAMESPACE"}) {
118 $namespace=$ENV{"XAPP_NAMESPACE"};
120 my $releaseName = "";
121 my $helmVersion = "0.0.1";
122 my $overrideFile = "";
125 # The curl command can be overridden for testing with a dummy.
129 Getopt::Long::Configure("no_auto_abbrev", "permute");
130 if (! GetOptions("h=s" => \$host,
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,
141 print "$myname: Error in options\n";
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";
165 # Verify command and call handler function
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,
183 print "$myname: Missing command\n";
188 # Variable status used for the return value of the whole script.
191 my $command = $ARGV[0];
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
198 print "$myname: Unrecognised command $command\n";
206 sub make_temp_name($) {
207 my $tmpsuffix = "${$}${^T}";
208 return "$_[0].$tmpsuffix";
212 $errfile = make_temp_name("/tmp/appmgr_e");
213 $resultfile = make_temp_name("/tmp/appmgr_r");
217 unlink ($errfile, $resultfile);
221 my $outputhandle = $_[0];
222 my $filename = $_[1];
225 if (!open($inhandle, "<", $filename)) {
226 print $outputhandle "$myname print_file: cannot open $filename: $!\n";
229 while (read($inhandle, $buffer, 4000) > 0) {
230 print $outputhandle $buffer;
235 # The HTTP protocol result code, filled in by rest().
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.
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]+)/) {
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.
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.
271 my $data = $_[2] || "";
273 my $http_status_file = make_temp_name("/tmp/appmgr_h");
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: $!";
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: $!";
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}");
292 push(@args, "--data");
296 print OLDSTDOUT "Running: " . join(" ", @args) . "\n";
298 if (system(@args) == -1) {
299 print OLDSTDOUT "$myname: failed to execute @args\n";
303 printf OLDSTDOUT "$myname: child died with signal %d, %s coredump\n",
304 ($? & 127), ($? & 128) ? 'with' : 'without';
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";
316 elsif ($http_code eq "") {
317 print OLDSTDOUT "$myname: curl failed to provide HTTP code\n";
322 print OLDSTDOUT "HTTP status code = $http_code\n";
324 $retval = 1; # Interaction OK from REST point of view
328 print_file(\*OLDSTDOUT, $errfile);
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);
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.
344 my $filename = $_[0];
345 my ($line, $inhandle, $outhandle);
346 if (!open($inhandle, "<", $filename)) {
347 print "$myname print_json: cannot open $filename: $!\n";
350 if (!open($outhandle, "|json_reformat")) {
351 print "$myname print_json: cannot pipe to json_reformat: $!\n";
354 while ($line = <$inhandle>) {
355 if (! ($line =~ /^##[0-9]+/)) {
356 print $outhandle $line;
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.
367 sub append_option($$) {
370 my $val = eval("\$$var");
372 $result = "$result,\"$var\":\"$val\"";
378 # Assumes the API currently implemented.
379 # Functions for each command below
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).
394 my $name = $_[0] || "";
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");
403 if (rest("POST", $base_xapps, $data)) {
404 if ($http_code eq "201") {
405 print_json $resultfile;
410 if ($http_code eq "400") {
411 $error = "INVALID PARAMETERS SUPPLIED";
413 elsif ($http_code eq "500") {
414 $error = "INTERNAL ERROR";
417 $error = "UNKNOWN STATUS $http_code";
429 print "$myname: Error: expected the name of xapp to deploy\n";
435 my $name = $_[0] || "";
436 my $urlpath = $base_xapps;
439 $urlpath = "$urlpath/$name";
440 if (rest("DELETE", $urlpath)) {
441 if ($http_code eq "204") {
442 print "SUCCESSFUL DELETION\n";
447 if ($http_code eq "400") {
448 $error = "INVALID XAPP NAME SUPPLIED";
450 elsif ($http_code eq "500") {
451 $error = "INTERNAL ERROR";
454 $error = "UNKNOWN STATUS $http_code";
466 print "$myname: Error: expected the name of xapp to undeploy\n";
472 my $name = $_[0] || "";
473 my $instance = $_[1] || "";
474 my $urlpath = $base_xapps;
477 $urlpath = "$urlpath/$name";
479 if ($instance ne "") {
480 $urlpath = "$urlpath/instances/$instance"
483 if (rest("GET", $urlpath)) {
484 if ($http_code eq "200") {
485 print_json $resultfile;
490 if ($http_code eq "400") {
491 $error = "INVALID XAPP NAME SUPPLIED";
493 if ($http_code eq "404") {
494 $error = "XAPP NOT FOUND";
496 elsif ($http_code eq "500") {
497 $error = "INTERNAL ERROR";
500 $error = "UNKNOWN STATUS $http_code";
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.
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] || "";
527 if (! ($targetUrl =~ /^http:\/\/.*/ or $targetUrl =~ /^https:\/\/.*/)) {
528 print "$myname: bad URL $targetUrl\n";
531 if ($eventType ne "created" and $eventType ne "deleted" and
532 $eventType ne "all") {
533 print "$myname: unrecognized event $eventType\n";
536 if (! ($maxRetries =~ /^[0-9]+$/)) {
537 print "$myname: invalid maximum retries count $maxRetries\n";
540 if (! ($retryTimer =~ /^[0-9]+$/)) {
541 print "$myname: invalid retry time $retryTimer\n";
547 # Format a subscriptionRequest JSON object
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}}";
558 # $1 is sub-command: list, add, delete, modify
560 sub do_subscriptions(@) {
561 my $subcommand = $_[0] || "";
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
572 if (exists $subcommands{$subcommand}) {
573 $subcommands{$subcommand}(@_);
576 print "$myname: unrecognized subscriptions subcommand $subcommand\n";
582 # list: With empty parameter, list all, else the parameter is
585 sub do_subscription_list(@) {
586 my $urlpath=$base_subs;
587 my $subscriptionId = $_[0] || "";
588 if ($subscriptionId ne "") {
589 $urlpath = "$urlpath/$subscriptionId";
592 if (rest("GET", $urlpath)) {
593 if ($http_code eq "200") {
594 print_json $resultfile;
599 if ($http_code eq "400") {
600 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
602 elsif ($http_code eq "404") {
603 $error = "SUBSCRIPTION $subscriptionId NOT FOUND";
605 elsif ($http_code eq "500") {
606 $error = "INTERNAL ERROR";
609 $error = "UNKNOWN STATUS $http_code";
621 sub do_subscription_add(@) {
622 my $urlpath=$base_subs;
624 if (validate_subscription(@_)) {
626 if (rest("POST", $urlpath, make_subscriptionRequest(@_))) {
627 if ($http_code eq "201") {
628 print_json $resultfile;
633 if ($http_code eq "400") {
634 $error = "INVALID INPUT";
636 elsif ($http_code eq "500") {
637 $error = "INTERNAL ERROR";
640 $error = "UNKNOWN STATUS $http_code";
656 sub do_subscription_delete(@) {
657 my $urlpath=$base_subs;
658 my $subscriptionId = $_[0] || "";
659 if ($subscriptionId ne "") {
660 $urlpath = "$urlpath/$subscriptionId";
663 print "$myname: delete: Subscription id required\n";
668 if (rest("DELETE", $urlpath)) {
669 if ($http_code eq "204") {
670 print "SUBSCRIPTION $subscriptionId DELETED\n";
675 if ($http_code eq "400") {
676 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
678 elsif ($http_code eq "500") {
679 $error = "INTERNAL ERROR";
682 $error = "UNKNOWN STATUS $http_code";
694 sub do_subscription_modify(@) {
695 my $urlpath=$base_subs;
697 $urlpath = "$urlpath/$_[0]";
700 print "$myname: modify: Subscription id required\n";
705 if (validate_subscription(@_)) {
707 if (rest("PUT", $urlpath, make_subscriptionRequest(@_))) {
708 if ($http_code eq "200") {
709 print_json $resultfile;
714 if ($http_code eq "400") {
715 $error = "INVALID INPUT";
717 elsif ($http_code eq "500") {
718 $error = "INTERNAL ERROR";
721 $error = "UNKNOWN STATUS $http_code";
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";
747 $urlpath = "$urlpath/$check";
749 if (rest("GET", $urlpath)) {
751 if ($check eq "alive") {
752 # If GET succeeds at all, the xapp manager is alive, no
753 # need to check the HTTP code.
757 if ($http_code eq "200") {
760 elsif ($http_code eq "503") {
763 elsif ($http_code eq "500") {
764 $res = "INTERNAL ERROR";
767 $res = "UNKNOWN STATUS $http_code";
774 print "$myname: health check failed to contact appmgr\n";
780 my $subcommand = $_[0] || "";
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
791 if (exists $subcommands{$subcommand}) {
792 $subcommands{$subcommand}(@_);
795 print "$myname: unrecognized config subcommand $subcommand\n";
801 sub do_config_list(@) {
803 print "$myname: \"config list\" has no parameters\n";
808 if (rest("GET", $base_config)) {
809 if ($http_code eq "200") {
810 print_json $resultfile;
815 if ($http_code eq "500") {
816 $error = "INTERNAL ERROR";
819 $error = "UNKNOWN STATUS $http_code";
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
836 # $_[1] = configName (name of the configMap)
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.
847 # Check only the 3 names at the beginning of config add/modify/delete
848 sub validate_config_names(@) {
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";
862 sub validate_config(@) {
864 print "validate_config args @_\n";
867 print "$myname: config file $_[0] cannot be read: $!\n";
873 if (! validate_config_names(@_)) {
876 for (my $idx = 3; $idx <= 4; ++$idx) {
878 print "$myname: cannot read file $_[$idx]\n";
884 print "$myname: config add: 1 or 5 parameter expected\n";
890 # Generate JSON for the xAppConfig element (see API).
892 sub make_xAppConfigInfo($$$) {
893 return "{\"xAppName\":\"$_[0]\",\"configMapName\":\"$_[1]\",\"namespace\":\"$_[2]\"}";
896 sub make_xAppConfig(@) {
897 my $retval = "{\"xAppConfigInfo\":" . make_xAppConfigInfo($_[0],$_[1],$_[2]);
899 open($fh, "<", $_[3]) or die "failed to open $_[3]";
902 $retval = $retval . ",\"configSchema\":" . join("", @obj);
903 open($fh, "<", $_[4]) or die "failed to open $_[4]";
906 $retval = $retval . ",\"configMap\":" . join("", @obj) . "}";
909 sub do_config_add(@) {
912 $paramCount = validate_config(@_);
913 if ($paramCount > 0) {
915 if ($paramCount == 1) {
916 $xAppConfig = "\@$_[0]";
919 $xAppConfig = make_xAppConfig(@_);
922 if (rest("POST", $base_config, $xAppConfig)) {
923 if ($http_code eq "201") {
924 print_json $resultfile;
927 elsif ($http_code eq "422") { # Validation failed, details in result
928 print_json $resultfile;
933 if ($http_code eq "400") {
934 $error = "INVALID INPUT";
936 elsif ($http_code eq "500") {
937 $error = "INTERNAL ERROR";
940 $error = "UNKNOWN STATUS $http_code";
956 sub do_config_modify(@) {
959 $paramCount = validate_config(@_);
960 if ($paramCount > 0) {
962 if ($paramCount == 1) {
963 $xAppConfig = "\@$_[0]";
966 $xAppConfig = make_xAppConfig(@_);
969 if (rest("PUT", $base_config, $xAppConfig)) {
970 if ($http_code eq "200") {
971 print_json $resultfile;
974 elsif ($http_code eq "422") { # Validation failed, details in result
975 print_json $resultfile;
980 if ($http_code eq "400") {
981 $error = "INVALID INPUT";
983 elsif ($http_code eq "500") {
984 $error = "INTERNAL ERROR";
987 $error = "UNKNOWN STATUS $http_code";
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.
1008 sub do_config_delete(@) {
1009 my $xAppConfigInfo = "";
1011 if ($#_ != 0 and $#_ != 2) {
1012 print "$myname: wrong number of parameters for config delete\n";
1017 $xAppConfigInfo = "\@$_[0]";
1020 print "$myname: config file $_[0] cannot be read: $!\n";
1024 elsif (($#_ == 2) && validate_config_names(@_)) {
1025 $xAppConfigInfo = make_xAppConfigInfo($_[0],$_[1],$_[2]);
1028 print "$myname: bad parameters for config delete\n";
1031 if ($xAppConfigInfo ne "") {
1033 if (rest("DELETE", $base_config, $xAppConfigInfo)) {
1034 if ($http_code eq "204") {
1035 print "SUCCESFUL DELETION OF CONFIG\n";
1040 if ($http_code eq "400") {
1041 $error = "INVALID PARAMETERS SUPPLIED";
1043 elsif ($http_code eq "500") {
1044 $error = "INTERNAL ERROR";
1047 $error = "UNKNOWN STATUS $http_code";