X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=blobdiff_plain;f=scripts%2Fappmgrcli;fp=scripts%2Fappmgrcli;h=4b9637d0813b86a961d11261672ba06f07eaa68b;hb=34e4383c438f87023bc411d9b0baa4a828a7e306;hp=22f06779fe378f07a7e61c4db07d5418debb6c54;hpb=4703b1a7457cf072640adbc0f5487a0675f5b6d3;p=ric-plt%2Fappmgr.git diff --git a/scripts/appmgrcli b/scripts/appmgrcli index 22f0677..4b9637d 100755 --- a/scripts/appmgrcli +++ b/scripts/appmgrcli @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/perl -w # # Copyright (c) 2019 AT&T Intellectual Property. # Copyright (c) 2019 Nokia. @@ -19,26 +19,34 @@ ############################# # Simple cli for xapp manager # -# In addition to standard shell tools, requires packages "curl" and +# In addition to standard shell tools, requires basic Perl installation +# (Ubuntu package "perl-base", installed by default), packages "curl" and # "yajl-tools" (the second provides json_reformat on Ubuntu; on Red Hat-style # distributions install "yajl" instead). # -myname=appmgrcli +use strict; +use Getopt::Long; +use Fcntl; -usage() { - cat < /dev/null ; then - echo $myname: Option -$flag has no required value, or value begins with -, - echo - which is disallowed. - usage - exit 1 - fi - case $flag in - (h) host="$OPTARG" - ;; - (p) port="$OPTARG" - ;; - (v) verbose=1 - ;; - (*) - echo $myname: Bad option letter or required option argument missing. - usage - exit 1 - ;; - esac -done -# Get rid of the option part -shift $((OPTIND-1)) - -if [ $verbose = 1 ]; then - echo "host = $host" - echo "port = $port" -fi - -# Verify command - -case $1 in - (deploy|dep) - cmd=deploy - ;; - (undeploy|undep) - cmd=undeploy - ;; - (status|stat) - cmd=status - ;; - (subscriptions|subs) - cmd=subscriptions - ;; - (health|heal) - cmd=health - ;; - (config|upload) - cmd=config - ;; - (help) - usage - exit 0 - ;; - (*) - if [ "x$1" = "x" ]; then - echo "$myname: Missing command" - else - echo "$myname: Unrecognized command $1" - fi - usage - exit 1 - ;; -esac - -if [ $verbose = 1 ]; then - echo "Command $cmd params=$2" -fi - -errfile=`mktemp /tmp/appmgr_e.XXXXXXXXXX` -resultfile=`mktemp /tmp/appmgr_r.XXXXXXXXXX` +if (exists $ENV{"APPMGR_HOST"}) { + $host=$ENV{"APPMGR_HOST"}; +} +if (exists $ENV{"APPMGR_PORT"}) { + $port=$ENV{"APPMGR_PORT"}; +} + +# Overrides for some deploy parameters + +my $configName = ""; +my $namespace = "ricxapp"; +my $releaseName = ""; +my $helmVersion = "0.0.1"; +my $overrideFile = ""; +my $podHost = ""; + +# The curl command can be overridden for testing with a dummy. + +my $curl = "curl"; + +Getopt::Long::Configure("no_auto_abbrev", "permute"); +if (! GetOptions("h=s" => \$host, + "p=i" => \$port, + "c=s" => \$curl, + "ConfigName=s" => \$configName, + "Namespace=s" => \$namespace, + "ReleaseName=s" => \$releaseName, + "HelmVersion=s" => \$helmVersion, + "OverrideFile=s" => \$overrideFile, + "podHost=s" => \$podHost, + "help" => \$showhelp, + "v" => \$verbose)) { + print "$myname: Error in options\n"; + helphint(); + exit 1; +} + +if ($showhelp) { + usage(); + exit 0; +} + +if ($verbose) { + print "host = $host\n"; + print "port = $port\n"; + print "ConfigName = $configName\n"; + print "Namespace = $namespace\n"; + print "ReleaseName = $releaseName\n"; + print "HelmVersion = $helmVersion\n"; + print "OverrideFile = $overrideFile\n"; + print "podHost = $podHost\n"; + for (my $idx = 0; $idx <= $#ARGV; ++$idx) { + print "\$ARGV[$idx] = $ARGV[$idx]\n"; + } +} + +# Verify command and call handler function + +my %commands = ( + "deploy" => \&do_deploy, + "dep" => \&do_deploy, + "undeploy" => \&do_undeploy, + "undep" => \&do_undeploy, + "status" => \&do_status, + "stat" => \&do_status, + "subscriptions" => \&do_subscriptions, + "subs" => \&do_subscriptions, + "health" => \&do_health, + "heal" => \&do_health, + "config" => \&do_config, + "help" => \&usage +); + +if ($#ARGV < 0) { + print "$myname: Missing command\n"; + helphint(); + exit 1; +} + # Variable status used for the return value of the whole script. -status=0 +my $status = 0; + +my $command = $ARGV[0]; +shift; +if (exists $commands{$command}) { + # Call the handler function with the rest of the command line + $commands{$command}(@ARGV); + exit $status; # Default exit. A handler can exit also if more convenient +} +print "$myname: Unrecognised command $command\n"; +helphint(); +exit 1; + +my $errfile; +my $resultfile; + + +sub make_temp_name($) { + my $tmpsuffix = "${$}${^T}"; + return "$_[0].$tmpsuffix"; +} + +sub make_temps { + $errfile = make_temp_name("/tmp/appmgr_e"); + $resultfile = make_temp_name("/tmp/appmgr_r"); +} + +sub remove_temps { + unlink ($errfile, $resultfile); +} + +sub print_file($$) { + my $outputhandle = $_[0]; + my $filename = $_[1]; + my $buffer; + my $inhandle; + if (!open($inhandle, "<", $filename)) { + print $outputhandle "$myname print_file: cannot open $filename: $!\n"; + return; + } + while (read($inhandle, $buffer, 4000) > 0) { + print $outputhandle $buffer; + } + close($inhandle); +} + +# The HTTP protocol result code, filled in by rest(). + +my $http_code = ""; + +# Helper: Given a curl output file, extract the number from ##code line. +# return ERROR if file cannot be opened, or "" if no code found. + +sub find_http_code($) { + my ($fh, $line, $code); + open($fh, "<", $_[0]) or return "ERROR"; + while ($line = <$fh>) { + if ($line =~ /^##([0-9]+)/) { + return $1; + } + } + return ""; +} # Helper for command execution: # Do a rest call with "curl": $1 = method, $2 = path (without host and port # which come from variables), $3 data to POST if needed -# returns 0 if OK, and any returned data is in $resultfile -# else 1, and error message from curl is in $errfile, which is printed -# before returning the 1. -# Also sets $status to the return value. +# returns true (1) if OK, and any returned data is in $resultfile +# else 0, and error message from curl is in $errfile, which is printed +# before returning the 0. # # On curl options: --silent --show-error disables progress bar, but allows # error messages. --connect-timeout 20 limits waiting for connection to # 20 seconds. In practice connection will succeed almost immediately, # or in the case of wrong address not at all. +# To get the http code, using -w with format. The result comes at the end +# of the output, so "decorating" it for easier filtering. +# The code is put to global $http_code. # -rest() { - local data - if [ "x$3" != "x" ]; then - data="--data $3" - fi +sub rest($$_) { + my $method = $_[0]; + my $path = $_[1]; + my $data = $_[2] || ""; + my $retval = 1; + my $http_status_file = make_temp_name("/tmp/appmgr_h"); + + # This redirects stderr (fd 2) to $errfile, but saving normal stderr + # so that if can be restored. + open(OLDERR, ">&", \*STDERR) or die "Can't dup STDERR: $!"; + open(ERRFILE, ">", $errfile) or die "open errorfile failed"; + open(STDERR, ">&", \*ERRFILE) or die "Can't dup ERRFILE: $!"; + + # This redirects stdout (fd 1) to $http_status_file, but saving original + # so that if can be restored. + open(OLDSTDOUT, ">&", \*STDOUT) or die "Can't dup STDOUT: $!"; + open(HTTP_STATUS_FILE, ">", $http_status_file) or die "open http status file failed"; + open(STDOUT, ">&", \*HTTP_STATUS_FILE) or die "Can't dup HTTP_STATUS_FILE: $!"; + + my @args = ($curl, "--silent", "--show-error", "--connect-timeout", "20", + "--header", "Content-Type: application/json", "-X", $method, + "-o", $resultfile, "-w", '\n##%{http_code}\n', + "http://${host}:${port}${path}"); + if ($data ne "") { + push(@args, "--data"); + push(@args, $data); + } + if ($verbose) { + print OLDSTDOUT "Running: " . join(" ", @args) . "\n"; + } + if (system(@args) == -1) { + print OLDSTDOUT "$myname: failed to execute @args\n"; + $retval = 0; + } + elsif ($? & 127) { + printf OLDSTDOUT "$myname: child died with signal %d, %s coredump\n", + ($? & 127), ($? & 128) ? 'with' : 'without'; + $retval = 0; + } + else { + my $curl_exit_code = $? >> 8; + if ($curl_exit_code == 0) { + seek HTTP_STATUS_FILE, 0, 0; # Ensures flushing + $http_code = find_http_code($http_status_file); + if ($http_code eq "ERROR") { + print OLDSTDOUT "$myname: failed to open temp file $http_status_file\n"; + $retval = 0; + } + elsif ($http_code eq "") { + print OLDSTDOUT "$myname: curl failed to provide HTTP code\n"; + $retval = 0; + } + else { + if ($verbose) { + print OLDSTDOUT "HTTP status code = $http_code\n"; + } + $retval = 1; # Interaction OK from REST point of view + } + } + else { + print_file(\*OLDSTDOUT, $errfile); + $retval = 0; + } + } + open(STDOUT, ">&", \*OLDSTDOUT) or die "Can't dup OLDSTDOUT: $!"; + open(STDERR, ">&", \*OLDERR) or die "Can't dup OLDERR: $!"; + unlink($http_status_file); + return $retval; +} + +# Pretty-print a JSON file to stdout. +# (currently uses json_reformat command) +# Skips the ##httpcode line we make "curl" +# add in order to get access to the HTTP status. - if curl --silent --show-error --connect-timeout 20 --header "Content-Type: application/json" -X $1 -o $resultfile "http://${host}:${port}$2" $data 2> $errfile ;then - status=0 - else - cat $errfile - status=1 - fi - return $status +sub print_json($) { + my $filename = $_[0]; + my ($line, $inhandle, $outhandle); + if (!open($inhandle, "<", $filename)) { + print "$myname print_json: cannot open $filename: $!\n"; + return; + } + if (!open($outhandle, "|json_reformat")) { + print "$myname print_json: cannot pipe to json_reformat: $!\n"; + return; + } + while ($line = <$inhandle>) { + if (! ($line =~ /^##[0-9]+/)) { + print $outhandle $line; + } + } + close($outhandle); + close($inhandle); } -remove_temps () { - rm -f $errfile $resultfile +# Append an entry like ","name":"value" to the first parameter, if "name" +# names a variable with non-empty value. +# Else returns the unmodified first parameter. + +sub append_option($$) { + my $result = $_[0]; + my $var = $_[1]; + my $val = eval("\$$var"); + if ($val ne "") { + $result = "$result,\"$var\":\"$val\""; + } + return $result; } -# Execute command ($cmd guaranteed to be valid) +# Command handlers # Assumes the API currently implemented. -# Functions for each command below (except health which is so simple). - -base=/ric/v1 -base_xapps=$base/xapps -base_health=$base/health -base_subs=$base/subscriptions -base_config=$base/config - -do_deploy() { - if [ "x$1" != "x" ]; then - if rest POST $base_xapps \{\"name\":\"$1\"\} ; then - json_reformat < $resultfile - fi - else - echo Error: expected the name of xapp to deploy - status=1 - fi -} - -do_undeploy() { - local urlpath - - urlpath=$base_xapps - if [ "x$1" != "x" ]; then - urlpath="$urlpath/$1" - if rest DELETE $urlpath; then - # Currently appmgr returns an empty result if - # undeploy is succesfull. Don't reformat file if empty. - if [ -s $resultfile ]; then - json_reformat < $resultfile - else - echo "$1 undeployed" - fi - fi - else - echo Error: expected the name of xapp to undeploy - status=1 - fi -} - -do_status() { - local urlpath - - urlpath=$base_xapps - if [ "x$1" != "x" ]; then - urlpath="$urlpath/$1" - fi - if [ "x$2" != "x" ]; then - urlpath="$urlpath/instances/$2" - fi - if rest GET $urlpath; then - json_reformat < $resultfile - fi -} - -# This is a bit more complex. $1 is sub-command: list, add, delete, modify +# Functions for each command below + +# Deploy: +# The deploy command has one mandatory parameter "name" in the API, +# and several optional ones. Used mainly internally for testing, because +# they all override Helm chart values: +# "helmVersion": Helm chart version to be used +# "releaseName": The releas name of xApp visible in K8s +# "namespace": Name of the namespace to which xApp is deployed. +# "overrideFile": The file content used to override values.yaml file +# this host from the host the xapp manager is running in, we use the term +# and variable name "podHost" here. +# The options come from options (see GetOptions() call). + +sub do_deploy(@) { + my $name = $_[0] || ""; + if ($name ne "") { + my $data = "{\"XappName\":\"$name\""; + $data = append_option($data, "helmVersion"); + $data = append_option($data, "releaseName"); + $data = append_option($data, "namespace"); + $data = append_option($data, "overrideFile"); + $data = $data . "}"; + make_temps(); + if (rest("POST", $base_xapps, $data)) { + if ($http_code eq "201") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID PARAMETERS SUPPLIED"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + print "$myname: Error: expected the name of xapp to deploy\n"; + $status = 1; + } +} + +sub do_undeploy(@) { + my $name = $_[0] || ""; + my $urlpath = $base_xapps; + if ($name ne "") { + make_temps(); + $urlpath = "$urlpath/$name"; + if (rest("DELETE", $urlpath)) { + if ($http_code eq "204") { + print "SUCCESSFUL DELETION\n"; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID XAPP NAME SUPPLIED"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status = 1; + } + remove_temps(); + } + else { + print "$myname: Error: expected the name of xapp to undeploy\n"; + $status = 1; + } +} +sub do_status(@) { + my $name = $_[0] || ""; + my $instance = $_[1] || ""; + my $urlpath = $base_xapps; + + if ($name ne "") { + $urlpath = "$urlpath/$name"; + } + if ($instance ne "") { + $urlpath = "$urlpath/instances/$instance" + } + make_temps(); + if (rest("GET", $urlpath)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID XAPP NAME SUPPLIED"; + } + if ($http_code eq "404") { + $error = "XAPP NOT FOUND"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status = 1; + } + remove_temps(); +} + +# Helpers for subscription: # Validate the subscription data that follows a subscription add or modify # subcommand. $1=URL, $2=eventType, $3=maxRetries, $4=retryTimer # URL must look like URL, event type must be one of created deleted all, # maxRetries and retryTimer must be non-negative numbers. -# If errors, sets variable status=1 and prints errors, else leaves -# status unchanged. +# If errors, returns false (0) and prints errors, else returns 1. # -validate_subscription() { - if ! expr "$1" : "^http://.*" \| "$1" : "^https://.*" >/dev/null; then - echo "$myname: bad URL $1" - status=1 - fi - if ! [ "$2" = created -o "$2" = deleted -o "$2" = all ]; then - echo "$myname: unrecognized event $2" - status=1 - fi - if ! expr "$3" : "^[0-9][0-9]*$" >/dev/null; then - echo "$myname: invalid maximum retries count $3" - status=1 - fi - if ! expr "$4" : "^[0-9][0-9]*$" >/dev/null; then - echo "$myname: invalid retry time $4" - status=1 - fi -} - -do_subscriptions() { - local urlpath - urlpath=$base_subs - case $1 in - (list) - if [ "x$2" != "x" ]; then - urlpath="$urlpath/$2" - fi - if rest GET $urlpath; then - json_reformat < $resultfile - else - status=1 - fi - ;; - (add) - validate_subscription "$2" "$3" "$4" "$5" - if [ $status = 0 ]; then - if rest POST $urlpath \{\"targetUrl\":\"$2\",\"eventType\":\"$3\",\"maxRetries\":$4,\"retryTimer\":$5\} ; then - json_reformat < $resultfile - else - status=1 - fi - fi - ;; - (delete|del) - if [ "x$2" != "x" ]; then - urlpath="$urlpath/$2" - else - echo "$myname: Subscription id required" - status=1 - fi - if [ $status = 0 ]; then - if rest DELETE $urlpath; then - # Currently appmgr returns an empty result if - # delete is succesfull. Don't reformat file if empty. - if [ -s $resultfile ]; then - json_reformat < $resultfile - else - echo "Subscription $2 deleted" - fi - else - status=1 - fi - fi - ;; - (modify|mod) - if [ "x$2" != "x" ]; then - urlpath="$urlpath/$2" - else - echo "$myname: Subscription id required" - status=1 - fi - if [ $status = 0 ]; then - validate_subscription "$3" "$4" "$5" "$6" - if [ $status = 0 ]; then - if rest PUT $urlpath \{\"targetUrl\":\"$3\",\"eventType\":\"$4\",\"maxRetries\":$5,\"retryTimer\":$6\} ; then - json_reformat < $resultfile - else - status=1 - fi - fi - fi - ;; - (*) - echo "$myname: unrecognized subscriptions subcommand $1" - status=1 - esac -} - -do_config() { - local urlpath - urlpath=$base_config - case $1 in - (get|list) - if [ "x$2" != "x" ]; then - urlpath="$urlpath/$2" - fi - if rest GET $urlpath; then - json_reformat < $resultfile - else - status=1 - fi - ;; - (add|update) - if rest POST $urlpath "@$2" ; then - cat $resultfile - else - status=1 - fi - ;; - (del|delete|remove|rem) - if rest DELETE $urlpath "@$2" ; then - cat $resultfile - else - status=1 - fi - ;; - (*) - echo "$myname: unrecognized config subcommand $1" - status=1 - esac -} - -case $cmd in - (deploy) - do_deploy "$2" - ;; - (undeploy) - do_undeploy "$2" - ;; - (status) - do_status "$2" "$3" - ;; - (subscriptions) - do_subscriptions "$2" "$3" "$4" "$5" "$6" "$7" - ;; - (config) - do_config "$2" "$3" - ;; - (health) - if rest GET $base_health ; then - echo OK - else - echo NOT OK - fi - ;; -esac -remove_temps -exit $status - -# An Emacs hack to set the indentation style of this file -# Local Variables: -# sh-indentation:2 -# End: +sub validate_subscription(@) { + # Using the API parameter names + my $targetUrl = $_[0] || ""; + my $eventType = $_[1] || ""; + my $maxRetries = $_[2] || ""; + my $retryTimer = $_[3] || ""; + my $retval = 1; + + if (! ($targetUrl =~ /^http:\/\/.*/ or $targetUrl =~ /^https:\/\/.*/)) { + print "$myname: bad URL $targetUrl\n"; + $retval = 0; + } + if ($eventType ne "created" and $eventType ne "deleted" and + $eventType ne "all") { + print "$myname: unrecognized event $eventType\n"; + $retval = 0; + } + if (! ($maxRetries =~ /^[0-9]+$/)) { + print "$myname: invalid maximum retries count $maxRetries\n"; + $retval = 0; + } + if (! ($retryTimer =~ /^[0-9]+$/)) { + print "$myname: invalid retry time $retryTimer\n"; + $retval = 0; + } + return $retval; +} + +# Format a subscriptionRequest JSON object + +sub make_subscriptionRequest(@) { + my $targetUrl = $_[0]; + my $eventType = $_[1]; + my $maxRetries = $_[2]; + my $retryTimer = $_[3]; + return "{\"Data\": {\"TargetUrl\":\"$targetUrl\",\"EventType\":\"$eventType\",\"MaxRetries\":$maxRetries,\"RetryTimer\":$retryTimer}}"; +} + +# Subscriptions: +# $1 is sub-command: list, add, delete, modify + +sub do_subscriptions(@) { + my $subcommand = $_[0] || ""; + shift; + + my %subcommands = ( + "list" => \&do_subscription_list, + "add" => \&do_subscription_add, + "delete" => \&do_subscription_delete, + "del" => \&do_subscription_delete, + "modify" => \&do_subscription_modify, + "mod" => \&do_subscription_modify + ); + if (exists $subcommands{$subcommand}) { + $subcommands{$subcommand}(@_); + } + else { + print "$myname: unrecognized subscriptions subcommand $subcommand\n"; + helphint(); + $status=1 + } +} + +# list: With empty parameter, list all, else the parameter is +# a subscriptionId + +sub do_subscription_list(@) { + my $urlpath=$base_subs; + my $subscriptionId = $_[0] || ""; + if ($subscriptionId ne "") { + $urlpath = "$urlpath/$subscriptionId"; + } + make_temps(); + if (rest("GET", $urlpath)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID SUBSCRIPTION ID $subscriptionId"; + } + elsif ($http_code eq "404") { + $error = "SUBSCRIPTION $subscriptionId NOT FOUND"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); +} + +sub do_subscription_add(@) { + my $urlpath=$base_subs; + + if (validate_subscription(@_)) { + make_temps(); + if (rest("POST", $urlpath, make_subscriptionRequest(@_))) { + if ($http_code eq "201") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +sub do_subscription_delete(@) { + my $urlpath=$base_subs; + my $subscriptionId = $_[0] || ""; + if ($subscriptionId ne "") { + $urlpath = "$urlpath/$subscriptionId"; + } + else { + print "$myname: delete: Subscription id required\n"; + $status=1; + return; + } + make_temps(); + if (rest("DELETE", $urlpath)) { + if ($http_code eq "204") { + print "SUBSCRIPTION $subscriptionId DELETED\n"; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID SUBSCRIPTION ID $subscriptionId"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status = 1; + } + remove_temps(); +} + +sub do_subscription_modify(@) { + my $urlpath=$base_subs; + if (defined $_[0]) { + $urlpath = "$urlpath/$_[0]"; + } + else { + print "$myname: modify: Subscription id required\n"; + $status=1; + return; + } + shift; + if (validate_subscription(@_)) { + make_temps(); + if (rest("PUT", $urlpath, make_subscriptionRequest(@_))) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +sub do_health(@) { + my $urlpath=$base_health; + my $check = $_[0] || ""; + # API now defines two types of checks, either of + # which must be specified. + if ($check ne "alive" and $check ne "ready") { + print "$myname: health check type required (alive or ready)\n"; + $status=1; + return; + } + $urlpath = "$urlpath/$check"; + make_temps(); + if (rest("GET", $urlpath)) { + my $res; + if ($check eq "alive") { + # If GET succeeds at all, the xapp manager is alive, no + # need to check the HTTP code. + $res = "ALIVE"; + } + else { + if ($http_code eq "200") { + $res = "READY"; + } + elsif ($http_code eq "503") { + $res = "NOT READY"; + } + elsif ($http_code eq "500") { + $res = "INTERNAL ERROR"; + } + else { + $res = "UNKNOWN STATUS $http_code"; + } + } + print "$res\n"; + } + else { + $status = 1; + print "$myname: health check failed to contact appmgr\n"; + } + remove_temps(); +} + +sub do_config(@) { + my $subcommand = $_[0] || ""; + shift; + + my %subcommands = ( + "list" => \&do_config_list, + "add" => \&do_config_add, + "delete" => \&do_config_delete, + "del" => \&do_config_delete, + "modify" => \&do_config_modify, + "mod" => \&do_config_modify + ); + if (exists $subcommands{$subcommand}) { + $subcommands{$subcommand}(@_); + } + else { + print "$myname: unrecognized config subcommand $subcommand\n"; + helphint(); + $status=1 + } +} + +sub do_config_list(@) { + if (defined $_[0]) { + print "$myname: \"config list\" has no parameters\n"; + $status = 1; + return; + } + make_temps(); + if (rest("GET", $base_config)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + else { + my $error; + if ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); +} + +# validate_config() checks configuration commmand line. +# "config add" and "config modify" expect either single parameter which +# must be a JSON file that contains the whole thing to send (see API), +# or 5 parameters, where the first three are +# $_[0] = name +# $_[1] = configName (name of the configMap) +# $_[2] = namespace +# Followed by two file names: +# $_[3] = file containing configSchema +# $_[4] = file containing data for configMap +# Giving the last two literally on the command line does not make much sense, +# since they are arbitrary JSON data. +# On success, returns parameter count (1 or 5), depending on which kind of +# command line found. +# 0 if errors. + +# Check only the 3 names at the beginning of config add/modify/delete +sub validate_config_names(@) { + my $retval = 1; + # Names in the Kubernetes world consist of lowercase alphanumerics + # and - and . as specified in + # https://kubernetes.io/docs/concepts/overview/working-with-objects/name + for (my $idx = 0; $idx <= 2; ++$idx) { + if (! ($_[$idx] =~ /^[a-z][-a-z0-9.]*$/)) { + print "$myname: invalid characters in name $_[$idx]\n"; + $retval = 0; + } + } + return $retval; +} + +sub validate_config(@) { + my $retval = 1; + print "validate_config args @_\n"; + if ($#_ == 0) { + if (! -r $_[0]) { + print "$myname: config file $_[0] cannot be read: $!\n"; + $retval = 0; + } + } + elsif ($#_ == 4) { + $retval = 5; + if (! validate_config_names(@_)) { + $retval = 0; + } + for (my $idx = 3; $idx <= 4; ++$idx) { + if (! -r $_[$idx]) { + print "$myname: cannot read file $_[$idx]\n"; + $retval = 0; + } + } + } + else { + print "$myname: config add: 1 or 5 parameter expected\n"; + $retval = 0; + } + return $retval; +} + +# Generate JSON for the xAppConfig element (see API). + +sub make_xAppConfigInfo($$$) { + return "{\"xAppName\":\"$_[0]\",\"configMapName\":\"$_[1]\",\"namespace\":\"$_[2]\"}"; +} + +sub make_xAppConfig(@) { + my $retval = "{\"xAppConfigInfo\":" . make_xAppConfigInfo($_[0],$_[1],$_[2]); + my $fh; + open($fh, "<", $_[3]) or die "failed to open $_[3]"; + my @obj = <$fh>; + close($fh); + $retval = $retval . ",\"configSchema\":" . join("", @obj); + open($fh, "<", $_[4]) or die "failed to open $_[4]"; + @obj = <$fh>; + close($fh); + $retval = $retval . ",\"configMap\":" . join("", @obj) . "}"; +} + +sub do_config_add(@) { + my $paramCount; + + $paramCount = validate_config(@_); + if ($paramCount > 0) { + my $xAppConfig; + if ($paramCount == 1) { + $xAppConfig = "\@$_[0]"; + } + else { + $xAppConfig = make_xAppConfig(@_); + } + make_temps(); + if (rest("POST", $base_config, $xAppConfig)) { + if ($http_code eq "201") { + print_json $resultfile; + $status = 0; + } + elsif ($http_code eq "422") { # Validation failed, details in result + print_json $resultfile; + $status = 1; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +sub do_config_modify(@) { + my $paramCount; + + $paramCount = validate_config(@_); + if ($paramCount > 0) { + my $xAppConfig; + if ($paramCount == 1) { + $xAppConfig = "\@$_[0]"; + } + else { + $xAppConfig = make_xAppConfig(@_); + } + make_temps(); + if (rest("PUT", $base_config, $xAppConfig)) { + if ($http_code eq "200") { + print_json $resultfile; + $status = 0; + } + elsif ($http_code eq "422") { # Validation failed, details in result + print_json $resultfile; + $status = 1; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID INPUT"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } + else { + $status = 1; + } +} + +# In config delete, allow either 1 parameter naming a file that contains +# a JSON xAppConfigInfo object, or 3 parameters giving the +# components (xAppName, configMapName, namespace), same as +# in add and modify operations. + +sub do_config_delete(@) { + my $xAppConfigInfo = ""; + + if ($#_ != 0 and $#_ != 2) { + print "$myname: wrong number of parameters for config delete\n"; + $status = 1; + } + elsif ($#_ == 0) { + if (-r $_[0]) { + $xAppConfigInfo = "\@$_[0]"; + } + else { + print "$myname: config file $_[0] cannot be read: $!\n"; + $status = 1; + } + } + elsif (($#_ == 2) && validate_config_names(@_)) { + $xAppConfigInfo = make_xAppConfigInfo($_[0],$_[1],$_[2]); + } + else { + print "$myname: bad parameters for config delete\n"; + $status = 1; + } + if ($xAppConfigInfo ne "") { + make_temps(); + if (rest("DELETE", $base_config, $xAppConfigInfo)) { + if ($http_code eq "204") { + print "SUCCESFUL DELETION OF CONFIG\n"; + $status = 0; + } + else { + my $error; + if ($http_code eq "400") { + $error = "INVALID PARAMETERS SUPPLIED"; + } + elsif ($http_code eq "500") { + $error = "INTERNAL ERROR"; + } + else { + $error = "UNKNOWN STATUS $http_code"; + } + print "$error\n"; + $status = 1; + } + } + else { + $status=1; + } + remove_temps(); + } +}