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 my $releaseName = "";
117 my $helmVersion = "0.0.1";
118 my $overrideFile = "";
121 # The curl command can be overridden for testing with a dummy.
125 Getopt::Long::Configure("no_auto_abbrev", "permute");
126 if (! GetOptions("h=s" => \$host,
129 "ConfigName=s" => \$configName,
130 "Namespace=s" => \$namespace,
131 "ReleaseName=s" => \$releaseName,
132 "HelmVersion=s" => \$helmVersion,
133 "OverrideFile=s" => \$overrideFile,
134 "podHost=s" => \$podHost,
135 "help" => \$showhelp,
137 print "$myname: Error in options\n";
148 print "host = $host\n";
149 print "port = $port\n";
150 print "ConfigName = $configName\n";
151 print "Namespace = $namespace\n";
152 print "ReleaseName = $releaseName\n";
153 print "HelmVersion = $helmVersion\n";
154 print "OverrideFile = $overrideFile\n";
155 print "podHost = $podHost\n";
156 for (my $idx = 0; $idx <= $#ARGV; ++$idx) {
157 print "\$ARGV[$idx] = $ARGV[$idx]\n";
161 # Verify command and call handler function
164 "deploy" => \&do_deploy,
165 "dep" => \&do_deploy,
166 "undeploy" => \&do_undeploy,
167 "undep" => \&do_undeploy,
168 "status" => \&do_status,
169 "stat" => \&do_status,
170 "subscriptions" => \&do_subscriptions,
171 "subs" => \&do_subscriptions,
172 "health" => \&do_health,
173 "heal" => \&do_health,
174 "config" => \&do_config,
179 print "$myname: Missing command\n";
184 # Variable status used for the return value of the whole script.
187 my $command = $ARGV[0];
189 if (exists $commands{$command}) {
190 # Call the handler function with the rest of the command line
191 $commands{$command}(@ARGV);
192 exit $status; # Default exit. A handler can exit also if more convenient
194 print "$myname: Unrecognised command $command\n";
202 sub make_temp_name($) {
203 my $tmpsuffix = "${$}${^T}";
204 return "$_[0].$tmpsuffix";
208 $errfile = make_temp_name("/tmp/appmgr_e");
209 $resultfile = make_temp_name("/tmp/appmgr_r");
213 unlink ($errfile, $resultfile);
217 my $outputhandle = $_[0];
218 my $filename = $_[1];
221 if (!open($inhandle, "<", $filename)) {
222 print $outputhandle "$myname print_file: cannot open $filename: $!\n";
225 while (read($inhandle, $buffer, 4000) > 0) {
226 print $outputhandle $buffer;
231 # The HTTP protocol result code, filled in by rest().
235 # Helper: Given a curl output file, extract the number from ##code line.
236 # return ERROR if file cannot be opened, or "" if no code found.
238 sub find_http_code($) {
239 my ($fh, $line, $code);
240 open($fh, "<", $_[0]) or return "ERROR";
241 while ($line = <$fh>) {
242 if ($line =~ /^##([0-9]+)/) {
249 # Helper for command execution:
250 # Do a rest call with "curl": $1 = method, $2 = path (without host and port
251 # which come from variables), $3 data to POST if needed
252 # returns true (1) if OK, and any returned data is in $resultfile
253 # else 0, and error message from curl is in $errfile, which is printed
254 # before returning the 0.
256 # On curl options: --silent --show-error disables progress bar, but allows
257 # error messages. --connect-timeout 20 limits waiting for connection to
258 # 20 seconds. In practice connection will succeed almost immediately,
259 # or in the case of wrong address not at all.
260 # To get the http code, using -w with format. The result comes at the end
261 # of the output, so "decorating" it for easier filtering.
262 # The code is put to global $http_code.
267 my $data = $_[2] || "";
269 my $http_status_file = make_temp_name("/tmp/appmgr_h");
271 # This redirects stderr (fd 2) to $errfile, but saving normal stderr
272 # so that if can be restored.
273 open(OLDERR, ">&", \*STDERR) or die "Can't dup STDERR: $!";
274 open(ERRFILE, ">", $errfile) or die "open errorfile failed";
275 open(STDERR, ">&", \*ERRFILE) or die "Can't dup ERRFILE: $!";
277 # This redirects stdout (fd 1) to $http_status_file, but saving original
278 # so that if can be restored.
279 open(OLDSTDOUT, ">&", \*STDOUT) or die "Can't dup STDOUT: $!";
280 open(HTTP_STATUS_FILE, ">", $http_status_file) or die "open http status file failed";
281 open(STDOUT, ">&", \*HTTP_STATUS_FILE) or die "Can't dup HTTP_STATUS_FILE: $!";
283 my @args = ($curl, "--silent", "--show-error", "--connect-timeout", "20",
284 "--header", "Content-Type: application/json", "-X", $method,
285 "-o", $resultfile, "-w", '\n##%{http_code}\n',
286 "http://${host}:${port}${path}");
288 push(@args, "--data");
292 print OLDSTDOUT "Running: " . join(" ", @args) . "\n";
294 if (system(@args) == -1) {
295 print OLDSTDOUT "$myname: failed to execute @args\n";
299 printf OLDSTDOUT "$myname: child died with signal %d, %s coredump\n",
300 ($? & 127), ($? & 128) ? 'with' : 'without';
304 my $curl_exit_code = $? >> 8;
305 if ($curl_exit_code == 0) {
306 seek HTTP_STATUS_FILE, 0, 0; # Ensures flushing
307 $http_code = find_http_code($http_status_file);
308 if ($http_code eq "ERROR") {
309 print OLDSTDOUT "$myname: failed to open temp file $http_status_file\n";
312 elsif ($http_code eq "") {
313 print OLDSTDOUT "$myname: curl failed to provide HTTP code\n";
318 print OLDSTDOUT "HTTP status code = $http_code\n";
320 $retval = 1; # Interaction OK from REST point of view
324 print_file(\*OLDSTDOUT, $errfile);
328 open(STDOUT, ">&", \*OLDSTDOUT) or die "Can't dup OLDSTDOUT: $!";
329 open(STDERR, ">&", \*OLDERR) or die "Can't dup OLDERR: $!";
330 unlink($http_status_file);
334 # Pretty-print a JSON file to stdout.
335 # (currently uses json_reformat command)
336 # Skips the ##httpcode line we make "curl"
337 # add in order to get access to the HTTP status.
340 my $filename = $_[0];
341 my ($line, $inhandle, $outhandle);
342 if (!open($inhandle, "<", $filename)) {
343 print "$myname print_json: cannot open $filename: $!\n";
346 if (!open($outhandle, "|json_reformat")) {
347 print "$myname print_json: cannot pipe to json_reformat: $!\n";
350 while ($line = <$inhandle>) {
351 if (! ($line =~ /^##[0-9]+/)) {
352 print $outhandle $line;
359 # Append an entry like ","name":"value" to the first parameter, if "name"
360 # names a variable with non-empty value.
361 # Else returns the unmodified first parameter.
363 sub append_option($$) {
366 my $val = eval("\$$var");
368 $result = "$result,\"$var\":\"$val\"";
374 # Assumes the API currently implemented.
375 # Functions for each command below
378 # The deploy command has one mandatory parameter "name" in the API,
379 # and several optional ones. Used mainly internally for testing, because
380 # they all override Helm chart values:
381 # "helmVersion": Helm chart version to be used
382 # "releaseName": The releas name of xApp visible in K8s
383 # "namespace": Name of the namespace to which xApp is deployed.
384 # "overrideFile": The file content used to override values.yaml file
385 # this host from the host the xapp manager is running in, we use the term
386 # and variable name "podHost" here.
387 # The options come from options (see GetOptions() call).
390 my $name = $_[0] || "";
392 my $data = "{\"XappName\":\"$name\"";
393 $data = append_option($data, "helmVersion");
394 $data = append_option($data, "releaseName");
395 $data = append_option($data, "namespace");
396 $data = append_option($data, "overrideFile");
399 if (rest("POST", $base_xapps, $data)) {
400 if ($http_code eq "201") {
401 print_json $resultfile;
406 if ($http_code eq "400") {
407 $error = "INVALID PARAMETERS SUPPLIED";
409 elsif ($http_code eq "500") {
410 $error = "INTERNAL ERROR";
413 $error = "UNKNOWN STATUS $http_code";
425 print "$myname: Error: expected the name of xapp to deploy\n";
431 my $name = $_[0] || "";
432 my $urlpath = $base_xapps;
435 $urlpath = "$urlpath/$name";
436 if (rest("DELETE", $urlpath)) {
437 if ($http_code eq "204") {
438 print "SUCCESSFUL DELETION\n";
443 if ($http_code eq "400") {
444 $error = "INVALID XAPP NAME SUPPLIED";
446 elsif ($http_code eq "500") {
447 $error = "INTERNAL ERROR";
450 $error = "UNKNOWN STATUS $http_code";
462 print "$myname: Error: expected the name of xapp to undeploy\n";
468 my $name = $_[0] || "";
469 my $instance = $_[1] || "";
470 my $urlpath = $base_xapps;
473 $urlpath = "$urlpath/$name";
475 if ($instance ne "") {
476 $urlpath = "$urlpath/instances/$instance"
479 if (rest("GET", $urlpath)) {
480 if ($http_code eq "200") {
481 print_json $resultfile;
486 if ($http_code eq "400") {
487 $error = "INVALID XAPP NAME SUPPLIED";
489 if ($http_code eq "404") {
490 $error = "XAPP NOT FOUND";
492 elsif ($http_code eq "500") {
493 $error = "INTERNAL ERROR";
496 $error = "UNKNOWN STATUS $http_code";
508 # Helpers for subscription:
509 # Validate the subscription data that follows a subscription add or modify
510 # subcommand. $1=URL, $2=eventType, $3=maxRetries, $4=retryTimer
511 # URL must look like URL, event type must be one of created deleted all,
512 # maxRetries and retryTimer must be non-negative numbers.
513 # If errors, returns false (0) and prints errors, else returns 1.
515 sub validate_subscription(@) {
516 # Using the API parameter names
517 my $targetUrl = $_[0] || "";
518 my $eventType = $_[1] || "";
519 my $maxRetries = $_[2] || "";
520 my $retryTimer = $_[3] || "";
523 if (! ($targetUrl =~ /^http:\/\/.*/ or $targetUrl =~ /^https:\/\/.*/)) {
524 print "$myname: bad URL $targetUrl\n";
527 if ($eventType ne "created" and $eventType ne "deleted" and
528 $eventType ne "all") {
529 print "$myname: unrecognized event $eventType\n";
532 if (! ($maxRetries =~ /^[0-9]+$/)) {
533 print "$myname: invalid maximum retries count $maxRetries\n";
536 if (! ($retryTimer =~ /^[0-9]+$/)) {
537 print "$myname: invalid retry time $retryTimer\n";
543 # Format a subscriptionRequest JSON object
545 sub make_subscriptionRequest(@) {
546 my $targetUrl = $_[0];
547 my $eventType = $_[1];
548 my $maxRetries = $_[2];
549 my $retryTimer = $_[3];
550 return "{\"Data\": {\"TargetUrl\":\"$targetUrl\",\"EventType\":\"$eventType\",\"MaxRetries\":$maxRetries,\"RetryTimer\":$retryTimer}}";
554 # $1 is sub-command: list, add, delete, modify
556 sub do_subscriptions(@) {
557 my $subcommand = $_[0] || "";
561 "list" => \&do_subscription_list,
562 "add" => \&do_subscription_add,
563 "delete" => \&do_subscription_delete,
564 "del" => \&do_subscription_delete,
565 "modify" => \&do_subscription_modify,
566 "mod" => \&do_subscription_modify
568 if (exists $subcommands{$subcommand}) {
569 $subcommands{$subcommand}(@_);
572 print "$myname: unrecognized subscriptions subcommand $subcommand\n";
578 # list: With empty parameter, list all, else the parameter is
581 sub do_subscription_list(@) {
582 my $urlpath=$base_subs;
583 my $subscriptionId = $_[0] || "";
584 if ($subscriptionId ne "") {
585 $urlpath = "$urlpath/$subscriptionId";
588 if (rest("GET", $urlpath)) {
589 if ($http_code eq "200") {
590 print_json $resultfile;
595 if ($http_code eq "400") {
596 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
598 elsif ($http_code eq "404") {
599 $error = "SUBSCRIPTION $subscriptionId NOT FOUND";
601 elsif ($http_code eq "500") {
602 $error = "INTERNAL ERROR";
605 $error = "UNKNOWN STATUS $http_code";
617 sub do_subscription_add(@) {
618 my $urlpath=$base_subs;
620 if (validate_subscription(@_)) {
622 if (rest("POST", $urlpath, make_subscriptionRequest(@_))) {
623 if ($http_code eq "201") {
624 print_json $resultfile;
629 if ($http_code eq "400") {
630 $error = "INVALID INPUT";
632 elsif ($http_code eq "500") {
633 $error = "INTERNAL ERROR";
636 $error = "UNKNOWN STATUS $http_code";
652 sub do_subscription_delete(@) {
653 my $urlpath=$base_subs;
654 my $subscriptionId = $_[0] || "";
655 if ($subscriptionId ne "") {
656 $urlpath = "$urlpath/$subscriptionId";
659 print "$myname: delete: Subscription id required\n";
664 if (rest("DELETE", $urlpath)) {
665 if ($http_code eq "204") {
666 print "SUBSCRIPTION $subscriptionId DELETED\n";
671 if ($http_code eq "400") {
672 $error = "INVALID SUBSCRIPTION ID $subscriptionId";
674 elsif ($http_code eq "500") {
675 $error = "INTERNAL ERROR";
678 $error = "UNKNOWN STATUS $http_code";
690 sub do_subscription_modify(@) {
691 my $urlpath=$base_subs;
693 $urlpath = "$urlpath/$_[0]";
696 print "$myname: modify: Subscription id required\n";
701 if (validate_subscription(@_)) {
703 if (rest("PUT", $urlpath, make_subscriptionRequest(@_))) {
704 if ($http_code eq "200") {
705 print_json $resultfile;
710 if ($http_code eq "400") {
711 $error = "INVALID INPUT";
713 elsif ($http_code eq "500") {
714 $error = "INTERNAL ERROR";
717 $error = "UNKNOWN STATUS $http_code";
734 my $urlpath=$base_health;
735 my $check = $_[0] || "";
736 # API now defines two types of checks, either of
737 # which must be specified.
738 if ($check ne "alive" and $check ne "ready") {
739 print "$myname: health check type required (alive or ready)\n";
743 $urlpath = "$urlpath/$check";
745 if (rest("GET", $urlpath)) {
747 if ($check eq "alive") {
748 # If GET succeeds at all, the xapp manager is alive, no
749 # need to check the HTTP code.
753 if ($http_code eq "200") {
756 elsif ($http_code eq "503") {
759 elsif ($http_code eq "500") {
760 $res = "INTERNAL ERROR";
763 $res = "UNKNOWN STATUS $http_code";
770 print "$myname: health check failed to contact appmgr\n";
776 my $subcommand = $_[0] || "";
780 "list" => \&do_config_list,
781 "add" => \&do_config_add,
782 "delete" => \&do_config_delete,
783 "del" => \&do_config_delete,
784 "modify" => \&do_config_modify,
785 "mod" => \&do_config_modify
787 if (exists $subcommands{$subcommand}) {
788 $subcommands{$subcommand}(@_);
791 print "$myname: unrecognized config subcommand $subcommand\n";
797 sub do_config_list(@) {
799 print "$myname: \"config list\" has no parameters\n";
804 if (rest("GET", $base_config)) {
805 if ($http_code eq "200") {
806 print_json $resultfile;
811 if ($http_code eq "500") {
812 $error = "INTERNAL ERROR";
815 $error = "UNKNOWN STATUS $http_code";
827 # validate_config() checks configuration commmand line.
828 # "config add" and "config modify" expect either single parameter which
829 # must be a JSON file that contains the whole thing to send (see API),
830 # or 5 parameters, where the first three are
832 # $_[1] = configName (name of the configMap)
834 # Followed by two file names:
835 # $_[3] = file containing configSchema
836 # $_[4] = file containing data for configMap
837 # Giving the last two literally on the command line does not make much sense,
838 # since they are arbitrary JSON data.
839 # On success, returns parameter count (1 or 5), depending on which kind of
840 # command line found.
843 # Check only the 3 names at the beginning of config add/modify/delete
844 sub validate_config_names(@) {
846 # Names in the Kubernetes world consist of lowercase alphanumerics
847 # and - and . as specified in
848 # https://kubernetes.io/docs/concepts/overview/working-with-objects/name
849 for (my $idx = 0; $idx <= 2; ++$idx) {
850 if (! ($_[$idx] =~ /^[a-z][-a-z0-9.]*$/)) {
851 print "$myname: invalid characters in name $_[$idx]\n";
858 sub validate_config(@) {
860 print "validate_config args @_\n";
863 print "$myname: config file $_[0] cannot be read: $!\n";
869 if (! validate_config_names(@_)) {
872 for (my $idx = 3; $idx <= 4; ++$idx) {
874 print "$myname: cannot read file $_[$idx]\n";
880 print "$myname: config add: 1 or 5 parameter expected\n";
886 # Generate JSON for the xAppConfig element (see API).
888 sub make_xAppConfigInfo($$$) {
889 return "{\"xAppName\":\"$_[0]\",\"configMapName\":\"$_[1]\",\"namespace\":\"$_[2]\"}";
892 sub make_xAppConfig(@) {
893 my $retval = "{\"xAppConfigInfo\":" . make_xAppConfigInfo($_[0],$_[1],$_[2]);
895 open($fh, "<", $_[3]) or die "failed to open $_[3]";
898 $retval = $retval . ",\"configSchema\":" . join("", @obj);
899 open($fh, "<", $_[4]) or die "failed to open $_[4]";
902 $retval = $retval . ",\"configMap\":" . join("", @obj) . "}";
905 sub do_config_add(@) {
908 $paramCount = validate_config(@_);
909 if ($paramCount > 0) {
911 if ($paramCount == 1) {
912 $xAppConfig = "\@$_[0]";
915 $xAppConfig = make_xAppConfig(@_);
918 if (rest("POST", $base_config, $xAppConfig)) {
919 if ($http_code eq "201") {
920 print_json $resultfile;
923 elsif ($http_code eq "422") { # Validation failed, details in result
924 print_json $resultfile;
929 if ($http_code eq "400") {
930 $error = "INVALID INPUT";
932 elsif ($http_code eq "500") {
933 $error = "INTERNAL ERROR";
936 $error = "UNKNOWN STATUS $http_code";
952 sub do_config_modify(@) {
955 $paramCount = validate_config(@_);
956 if ($paramCount > 0) {
958 if ($paramCount == 1) {
959 $xAppConfig = "\@$_[0]";
962 $xAppConfig = make_xAppConfig(@_);
965 if (rest("PUT", $base_config, $xAppConfig)) {
966 if ($http_code eq "200") {
967 print_json $resultfile;
970 elsif ($http_code eq "422") { # Validation failed, details in result
971 print_json $resultfile;
976 if ($http_code eq "400") {
977 $error = "INVALID INPUT";
979 elsif ($http_code eq "500") {
980 $error = "INTERNAL ERROR";
983 $error = "UNKNOWN STATUS $http_code";
999 # In config delete, allow either 1 parameter naming a file that contains
1000 # a JSON xAppConfigInfo object, or 3 parameters giving the
1001 # components (xAppName, configMapName, namespace), same as
1002 # in add and modify operations.
1004 sub do_config_delete(@) {
1005 my $xAppConfigInfo = "";
1007 if ($#_ != 0 and $#_ != 2) {
1008 print "$myname: wrong number of parameters for config delete\n";
1013 $xAppConfigInfo = "\@$_[0]";
1016 print "$myname: config file $_[0] cannot be read: $!\n";
1020 elsif (($#_ == 2) && validate_config_names(@_)) {
1021 $xAppConfigInfo = make_xAppConfigInfo($_[0],$_[1],$_[2]);
1024 print "$myname: bad parameters for config delete\n";
1027 if ($xAppConfigInfo ne "") {
1029 if (rest("DELETE", $base_config, $xAppConfigInfo)) {
1030 if ($http_code eq "204") {
1031 print "SUCCESFUL DELETION OF CONFIG\n";
1036 if ($http_code eq "400") {
1037 $error = "INVALID PARAMETERS SUPPLIED";
1039 elsif ($http_code eq "500") {
1040 $error = "INTERNAL ERROR";
1043 $error = "UNKNOWN STATUS $http_code";