-#!/bin/sh
+#!/usr/bin/perl -w
#
# Copyright (c) 2019 AT&T Intellectual Property.
# Copyright (c) 2019 Nokia.
#############################
# 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 <<EOF1
-usage: $myname [-h host] [-p port] [-v] command params...
-- command is one of deploy, undeploy, status, subscriptions, health, help
+my $myname="appmgrcli";
+
+sub usage {
+ print <<"EOF1";
+usage: $myname [-h host] [-p port] [-v] [-c curlprog] command params...
+- command is deploy, undeploy, status, subscriptions, health, config, help
- (abbreviations dep, undep, stat, subs, heal allowed)
- Parameters of the commands that may have parameters:
-- deploy: name of the xapp to deploy
+---- Deployment parameters come from the Help chart but the following can be
+---- overridden by option with the same name (for example --configName=cname):
+----- helmVersion ReleaseName namespace podHost (host of pods).
-- undeploy: name of the xapp to undeploy
-- status:
---- No parameters: Lists information about all deployed xapps
---- xapp name as parameter: Prints information about the given xapp
---- xapp name and instance: Lists information about the given instance only
-- subscriptions is followed by sub-command list, add, delete, or modify
----(abbreviations del and mod for delete and modify are allowed):
+--- (abbreviations del and mod for delete and modify are allowed):
---- list without parameters lists all subscriptions
---- list with subscription id prints that subscription
---- add URL eventType maxRetry retryTimer
--------the rest of the parameters are like in add
---- delete id
------- id is the subscription id to delete (find out with the list command)
+-- config is followed by sub-command list, add, delete, or modify
+--- (abbreviations del and mod for delete and modify are allowed):
+---- list (no pars)
+------ lists the configuration of all xapps
+---- add jsonfile
+------ Creates xapp configuration. the jsonfile must contain all data (see API)
+---- add name configName namespace configSchemaFile configDataFile
+------ Creates xapp configuration, but unlike in the 1-parameter form,
+------ Xapp name, config map name and namespace are separate parameters.
+------ The other data come from JSON files.
+---- modify
+------ Modifies existing configuration. Same parameters (1 or 5) as in add.
+---- delete name configName namespace
+------ Deletes the configuration identified by the parameters.
+--------------------------------------------------------------
- Default values for host and port can be set in environment
- variables APPMGR_HOST and APPMGR_PORT
- Option -v sets verbose mode.
+- Option -c overrides the used curl program name ("curl" by default).
+- Exit code is 0 for success, 1 for any kind of failure.
EOF1
+ exit 0;
+}
+
+sub helphint {
+ print "run $myname help (or --help) for instructions\n";
}
# Defaults
-host=localhost
-port=8080
-verbose=0
+my $host="localhost";
+my $port=8080;
+my $verbose=0;
+my $showhelp = 0;
+
+# API URLs
+
+my $base="/ric/v1";
+my $base_xapps="$base/xapps";
+my $base_health="$base/health";
+my $base_subs="$base/subscriptions";
+my $base_config="$base/config";
# Check for environment override
-if [ "x$APPMGR_HOST" != "x" ]; then
- host="$APPMGR_HOST"
-fi
-if [ "x$APPMGR_PORT" != "x" ]; then
- port="$APPMGR_PORT"
-fi
-
-# Proper shell option parsing:
-while getopts "h:p:v" flag
-do
- # Curiously, getopts does not guard against an argument-requiring option
- # eating the next option. It also does not handle the -- convention.
- # Here is how to fix that.
- if [ "X$OPTARG" = 'X--' ]; then
- break # Explicit end of options
- fi
- if expr -- "$OPTARG" : '-.*' > /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();
+ }
+}