push addional code
[sdc.git] / openecomp-be / tools / build / scripts / action_library_client / action_library_client.py
1 #!/usr/bin/python
2
3 ##############################################################################
4 #
5 # action_library_client.py
6 #
7 # A command-line client for the SDC Action Library.
8 #
9 #
10 # Usage:
11 #
12 #  Usage: action_library_client.py [--help] [--url <url>] [--in <filename>]
13 #                                  [--out <filename>] [--config <filename>]
14 #                                  [--log <filename>] [--uuid <uuid>]
15 #                                  [--curl] [--dryrun] [--verbose] [--version]
16 #                                  [--list | --create | --update= | --delete |
17 #                                   --checkout | --undocheckout | --checkin | --submit]
18 #
19 #  Optional arguments:
20 #    --help                Show this help message and exit
21 #    --url <url>           REST endpoint URL
22 #    --in <filename>       Path to JSON input file (else STDIN)
23 #    --out <filename>      Path to JSON output file (else STDOUT or logfile)
24 #    --config <filename>   Path to configuration file
25 #    --log <filename>      Path to logfile (else STDOUT)
26 #    --uuid <uuid>         Action UUID, (=='actionInvariantUUID')
27 #    --curl                Use curl transport impl
28 #    --dryrun              Describe what will happen, execute nothing
29 #    --verbose             Verbose diagnostic output
30 #    --version             Print script version and exit
31 #    --list                List actions
32 #    --create              Create new action (requires --in)
33 #    --update              Update existing action (requires --uuid, --in)
34 #    --delete              Delete existing action (requires --uuid)
35 #    --checkout            Create minor version candidate (requires --uuid)
36 #    --undocheckout        Discard minor version candidate (requires --uuid)
37 #    --checkin             Create minor version from candidate (requires --uuid)
38 #    --submit              Create next major version (requires --uuid)
39 #
40 # For example:
41 #
42 #    ./action_library_client.py --url http://10.147.97.199:8080 --list
43 #
44 # Output:
45 #   - Return values:
46 #      - 0 - OK
47 #      - 1 - GENERAL_ERROR
48 #      - 2 - ARGUMENTS_ERROR
49 #      - 3 - HTTP_FORBIDDEN_ERROR
50 #      - 4 - HTTP_BAD_REQUEST_ERROR
51 #      - 5 - HTTP_GENERAL_ERROR
52 #      - 6 - PROCESS_ERROR
53 #   - JSON - to stdout:
54 #      - Delimited by "----------"
55 #      - Delimiter overrideable with ALC_JSON_DELIMITER setting.
56 #
57 # Configuration/env settings:
58 #   - ALC_HTTP_USER - HTTP BASIC username
59 #   - ALC_HTTP_PASS - HTTP BASIC password
60 #   - ALC_HTTP_INSECURE - allow untrusted SSL (server) connections.
61 #   - ALC_TIMEOUT_SECONDS - invocation (e.g. HTTP) timeout in seconds.
62 #   - ALC_JSON_DELIMITER - JSON delimiter in ouput.
63 #   - ALC_ECOMP_INSTANCE_ID - X-ECOMP-InstanceID header
64 #
65 # Configuration by 0600-mode INI file (section "action_library_client") is preferred.
66 #
67 # See:
68 #    http://10.147.97.199:8080/api-docs/ - REST API Swagger docs
69 #    https://www.python.org/dev/peps/pep-0008/ - style guide
70 #    ../doc/SDC_Action_Lib_API_AID_1610_13.pdf - REST API dev guide
71 #
72 # Version history:
73 # - 1.0.0 November 28th 2016, LP, initial impl.
74 # - 1.0.1 November 29th 2016, LP, constants, documentation, add --version.
75 # - 1.0.2 November 29th 2016, LP, logging to files, stream-handling.
76 # - 1.0.3 November 30th 2016, LP, optionally read config from env or config file.
77 # - 1.1.0 December 3rd 2016, LP, backport from Python 3.4.2 to 2.6.6(!).
78 #
79 ##############################################################################
80
81
82 import sys
83 import os
84 import logging
85 import base64
86 import tempfile
87 import uuid
88 import json
89 import ssl
90 import urllib2
91 import subprocess
92 import ConfigParser
93 from abc import abstractmethod
94
95
96 ###############################################################################
97
98
99 class Constants(object):
100     """Common constants, for want of a better language feature..."""
101     # Values.
102     VERSION = "1.1.0"
103     APPLICATION = "action_library_client"
104     ACTIONS_URI = "onboarding-api/workflow/v1.0/actions"
105     ECOMP_INSTANCE_ID = "sdc_alc"
106     TIMEOUT_SECONDS_DEFAULT = 30
107     JSON_DELIMITER_DEFAULT = "----------"
108     LOG_FORMAT = "%(name)s\t%(levelname)s\t%(asctime)s\t%(message)s"
109     # Env variable names.
110     ENV_HTTP_USER = "ALC_HTTP_USER"
111     ENV_HTTP_PASS = "ALC_HTTP_PASS"
112     ENV_HTTP_INSECURE = "ALC_HTTP_INSECURE"
113     ENV_HTTP_CAFILE = "ALC_HTTP_CAFILE"
114     ENV_TIMEOUT_SECONDS = "ALC_TIMEOUT_SECONDS"
115     ENV_JSON_DELIMITER = "ALC_JSON_DELIMITER"
116     ENV_ECOMP_INSTANCE_ID = "ALC_ECOMP_INSTANCE_ID"
117
118
119 ###############################################################################
120
121
122 class ResponseCodes(object):
123     """Responses returned by IRESTClient impls."""
124     OK = 0
125     GENERAL_ERROR = 1
126     ARGUMENTS_ERROR = 2
127     HTTP_NOT_FOUND_ERROR = 3
128     HTTP_FORBIDDEN_ERROR = 4
129     HTTP_BAD_REQUEST_ERROR = 5
130     HTTP_GENERAL_ERROR = 6
131     PROCESS_GENERAL_ERROR = 9
132
133
134 ###############################################################################
135
136
137 class FinalizeStatus(object):
138     """Finalization operations."""
139     Checkout = "Checkout"
140     UndoCheckout = "Undo_Checkout"
141     CheckIn = "Checkin"
142     Submit = "Submit"
143
144
145 ###############################################################################
146
147
148 class ArgsDict(dict):
149     """A dict which makes attributes accessible as properties."""
150     def __getattr__(self, attr):
151         return self[attr]
152
153     def __setattr__(self, attr, value):
154         self[attr] = value
155
156
157 ###############################################################################
158
159
160 class ArgumentParser(object):
161     """A minimal reimpl of the argparse library, core in later Python releases"""
162     ACTIONS = ["list", "create", "update", "delete", "checkout", "undocheckout", "checkin", "submit"]
163     PARMS = ["url", "in", "out", "config", "log", "uuid"]
164     OTHER = ["curl", "dryrun", "verbose", "version", "help"]
165
166     def parse_args(self, clargs):
167         """Parse command-line args, returning a dict that exposes everything as properties."""
168         args = ArgsDict()
169         args.action = None
170         for arg in self.ACTIONS + self.PARMS + self.OTHER:
171             args[arg] = None
172         skip = False
173         try:
174             for i, clarg in enumerate(clargs):
175                 if skip:
176                     skip = False
177                     continue
178                 if not clarg.startswith("--"):
179                     raise Exception("Invalid argument: {0}".format(clarg))
180                 arg = str(clarg[2:])
181                 if arg in self.ACTIONS:
182                     if args.action:
183                         raise Exception("Duplicate actions: --{0}, {1}".format(args.action, clarg))
184                     args.action = arg
185                 elif arg in self.PARMS:
186                     try:
187                         args[arg] = clargs[i + 1]
188                         skip = True
189                     except IndexError:
190                         raise Exception("Option {0} requires an argument".format(clarg))
191                 elif arg in self.OTHER:
192                     args[arg] = True
193                 else:
194                     raise Exception("Invalid argument: {0}".format(clarg))
195
196             # Check action args.
197
198             if args.action:
199                 if not args.url:
200                     raise Exception("--url required for every action")
201                 if not args.uuid:
202                     if args.action not in ["create", "list"]:
203                         raise Exception("--uuid required for every action EXCEPT --list/--create")
204
205             # Read from file or stdin, and replace the problematic "in"
206             # property with "infile".
207
208             if args.action in ["create", "update"]:
209                 if args["in"]:
210                     args.infile = open(args["in"], mode="r")
211                 else:
212                     args.infile = sys.stdin
213
214         except Exception as e:
215             print(e)
216             ArgumentParser.usage()
217             sys.exit(ResponseCodes.ARGUMENTS_ERROR)
218         return args
219
220     @staticmethod
221     def usage():
222         """Print usage message."""
223         print("" +
224             "Usage: action_library_client.py [--help] [--url <url>] [--in <filename>]\n" +
225             "                                 [--out <filename>] [--config <filename>]\n" +
226             "                                 [--log <filename>] [--uuid <uuid>]\n" +
227             "                                 [--curl] [--dryrun] [--verbose] [--version]\n" +
228             "                                 [--list | --create | --update= | --delete |\n" +
229             "                                  --checkout | --undocheckout | --checkin | --submit]\n" +
230             "\n" +
231             "Optional arguments:\n" +
232             "  --help                Show this help message and exit\n" +
233             "  --url <url>           REST endpoint URL\n" +
234             "  --in <filename>       Path to JSON input file (else STDIN)\n" +
235             "  --out <filename>      Path to JSON output file (else STDOUT or logfile)\n" +
236             "  --config <filename>   Path to configuration file\n" +
237             "  --log <filename>      Path to logfile (else STDOUT)\n" +
238             "  --uuid <uuid>         Action UUID, (=='actionInvariantUUID')\n" +
239             "  --curl                Use curl transport impl\n" +
240             "  --dryrun              Describe what will happen, execute nothing\n" +
241             "  --verbose             Verbose diagnostic output\n" +
242             "  --version             Print script version and exit\n" +
243             "  --list                List actions\n" +
244             "  --create              Create new action (requires --in)\n" +
245             "  --update              Update existing action (requires --uuid, --in)\n" +
246             "  --delete              Delete existing action (requires --uuid)\n" +
247             "  --checkout            Create minor version candidate (requires --uuid)\n" +
248             "  --undocheckout        Discard minor version candidate (requires --uuid)\n" +
249             "  --checkin             Create minor version from candidate (requires --uuid)\n" +
250             "  --submit              Create next major version (requires --uuid)")
251
252
253 ###############################################################################
254
255
256 class Settings(object):
257     """Settings read from (optional) configfile, or environment."""
258
259     def __init__(self, args):
260         """Construct for command-line args."""
261         self.config = ConfigParser.ConfigParser()
262         if args.config:
263             self.config.read(args.config)
264
265     def get(self, name, default_value=None):
266         """Get setting from configfile or environment"""
267         try:
268             return self.config.get(Constants.APPLICATION, name)
269         except (KeyError, ConfigParser.NoSectionError, ConfigParser.NoOptionError):
270             try:
271                 return os.environ[name]
272             except KeyError:
273                 return default_value
274
275
276 ###############################################################################
277
278
279 # Python3: metaclass=ABCMeta
280 class IRESTClient(object):
281     """Base class for local, proxy and dryrun impls."""
282
283     def __init__(self, args):
284         self.args = args
285         self.logger = Runner.get_logger()
286         self.settings = Settings(args)
287
288     @abstractmethod
289     def list(self):
290         """Abstract list operation."""
291         pass
292
293     @abstractmethod
294     def create(self):
295         """Abstract list operation."""
296         pass
297
298     @abstractmethod
299     def update(self):
300         """Abstract list operation."""
301         pass
302
303     @abstractmethod
304     def delete(self):
305         """Abstract list operation."""
306         pass
307
308     @abstractmethod
309     def version(self, status):
310         """Abstract list operation."""
311         pass
312
313     @staticmethod
314     def new_uuid():
315         """Generate UUID."""
316         return str(uuid.uuid4())
317
318     def get_timeout_seconds(self):
319         """Get request timeout in seconds."""
320         return self.settings.get(Constants.ENV_TIMEOUT_SECONDS,
321                                  Constants.TIMEOUT_SECONDS_DEFAULT)
322
323     def get_http_insecure(self):
324         """Get whether SSL certificate checks are (inadvisably) disabled."""
325         return True if self.settings.get(Constants.ENV_HTTP_INSECURE) else False
326
327     def get_http_cafile(self):
328         """Get optional CA file for SSL server cert validation"""
329         if not self.get_http_insecure():
330             return self.settings.get(Constants.ENV_HTTP_CAFILE)
331
332     def get_basic_credentials(self):
333         """Generate Authorization: header."""
334         usr = self.settings.get(Constants.ENV_HTTP_USER)
335         pwd = self.settings.get(Constants.ENV_HTTP_PASS)
336         if usr and pwd:
337             return base64.b64encode(bytes("{0}:{1}".format(usr, pwd))).decode("ascii")
338         else:
339             raise Exception("REST service credentials not found")
340
341     def make_service_url(self):
342         """Generate service URL based on command-line arguments."""
343         url = self.args.url
344         if "/onboarding-api/" not in url:
345             separator = "" if url.endswith("/") else "/"
346             url = "{0}{1}{2}".format(url, separator, str(Constants.ACTIONS_URI))
347         if self.args.uuid:
348             separator = "" if url.endswith("/") else "/"
349             url = "{0}{1}{2}".format(url, separator, self.args.uuid)
350         return url
351
352     def log_json_response(self, method, json_dict):
353         """Log JSON response regardless of transport."""
354         json_str = json.dumps(json_dict, indent=4)
355         delimiter = self.settings.get(Constants.ENV_JSON_DELIMITER, Constants.JSON_DELIMITER_DEFAULT)
356         self.logger.info("HTTP {0} JSON response:\n{1}\n{2}\n{3}\n".format(method, delimiter, json_str, delimiter))
357         if self.args.out:
358             with open(self.args.out, "w") as tmp:
359                 tmp.write(json_str)
360                 tmp.flush()
361         elif self.args.log:
362             # Directly to stdout if logging is sent to a file.
363             print(json_str)
364
365     def log_action(self, action, status=None):
366         """Debug action before invocation."""
367         url = self.make_service_url()
368         name = status if status else self.__get_name()
369         self.logger.debug("{0}::{1}({2})".format(name, action, url))
370
371     @staticmethod
372     def _get_result_from_http_response(code):
373         """Get script returncode from HTTP error."""
374         if code == 400:
375             return ResponseCodes.HTTP_BAD_REQUEST_ERROR
376         elif code == 403:
377             return ResponseCodes.HTTP_FORBIDDEN_ERROR
378         elif code == 404:
379             return ResponseCodes.HTTP_NOT_FOUND_ERROR
380         return ResponseCodes.HTTP_GENERAL_ERROR
381
382     def __get_name(self):
383         """Get classname for diags"""
384         return type(self).__name__
385
386
387 ###############################################################################
388
389
390 class NativeRESTClient(IRESTClient):
391     """In-process IRESTClient impl."""
392
393     def list(self):
394         """In-process list impl."""
395         self.log_action("list")
396         return self.__exec(method="GET", expect_json=True)
397
398     def create(self):
399         """In-process create impl."""
400         self.log_action("create")
401         json_bytes = bytes(self.args.infile.read())
402         return self.__exec(method="POST", json_bytes=json_bytes, expect_json=True)
403
404     def update(self):
405         """In-process update impl."""
406         self.log_action("update")
407         json_bytes = bytes(self.args.infile.read())
408         return self.__exec(method="PUT", json_bytes=json_bytes, expect_json=True)
409
410     def delete(self):
411         """In-process delete impl."""
412         self.log_action("delete")
413         return self.__exec(method="DELETE")
414
415     def version(self, status):
416         """In-process version impl."""
417         self.log_action("version", status)
418         json_bytes = bytes(json.dumps({"status": status}))
419         return self.__exec(method="POST", json_bytes=json_bytes, expect_json=True)
420
421     def __exec(self, method, json_bytes=None, expect_json=None):
422         """Build command, execute it, validate and return response."""
423         try:
424             url = self.make_service_url()
425             timeout = float(self.get_timeout_seconds())
426             cafile = self.get_http_cafile()
427             headers = {
428                 "Content-Type": "application/json",
429                 "Accept": "application/json",
430                 "Authorization": "Basic {0}".format(self.get_basic_credentials()),
431                 "X-ECOMP-InstanceID": Constants.ECOMP_INSTANCE_ID,
432                 "X-ECOMP-RequestID": IRESTClient.new_uuid()
433             }
434
435             handler = urllib2.HTTPHandler
436             if hasattr(ssl, 'create_default_context'):
437                 ctx = ssl.create_default_context(cafile=cafile)
438                 if self.get_http_insecure():
439                     ctx.check_hostname = False
440                     ctx.verify_mode = ssl.CERT_NONE
441                 handler = urllib2.HTTPSHandler(context=ctx) if url.lower().startswith("https") else urllib2.HTTPHandler
442
443             self.logger.debug("URL {0} {1}: {2}".format(url, method, json_bytes))
444
445             opener = urllib2.build_opener(handler)
446             request = urllib2.Request(url, data=json_bytes, headers=headers)
447             request.get_method = lambda: method
448
449             f = None
450             try:
451                 f = opener.open(request, timeout=timeout)
452                 return self.__handle_response(f, method, expect_json)
453             finally:
454                 if f:
455                     f.close()
456
457         except urllib2.HTTPError as err:
458             self.logger.exception(err)
459             return IRESTClient._get_result_from_http_response(err.getcode())
460         except urllib2.URLError as err:
461             self.logger.exception(err)
462             return ResponseCodes.HTTP_GENERAL_ERROR
463
464     def __handle_response(self, f, method, expect_json):
465         """Devolve response handling because of the """
466         self.logger.debug("HTTP {0} status {1}, reason:\n{2}".format(method, f.getcode(), f.info()))
467         if expect_json:
468             # JSON responses get "returned", but actually it's the logging that
469             # most callers will be looking for.
470             json_body = json.loads(f.read().decode("utf-8"))
471             self.log_json_response(method, json_body)
472             return json_body
473         # Not JSON, but the operation succeeded, so return True.
474         return ResponseCodes.OK
475
476
477 ###############################################################################
478
479
480 class CURLRESTClient(IRESTClient):
481     """Remote/curl IRESTClient impl."""
482
483     def list(self):
484         """curl list impl"""
485         self.log_action("list")
486         return self._exec(method="GET", expect_json=True)
487
488     def create(self):
489         """curl create impl"""
490         self.log_action("create")
491         data_args = ["--data", "@{0}".format(self.args.infile.name)]
492         return self._exec(method="POST", extra_args=data_args, expect_json=True)
493
494     def update(self):
495         """curl update impl"""
496         self.log_action("update")
497         data_args = ["--data", "@{0}".format(self.args.infile.name)]
498         return self._exec(method="PUT", extra_args=data_args, expect_json=True)
499
500     def delete(self):
501         """curl delete impl"""
502         self.log_action("delete")
503         return self._exec(method="DELETE", expect_json=False)
504
505     def version(self, status):
506         """curl version impl"""
507         self.log_action("version", status)
508         with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
509             tmp.write(json.dumps({"status": status}))
510             tmp.flush()
511         data_args = ["--data", "@{0}".format(tmp.name)]
512         return self._exec(method="POST", extra_args=data_args, expect_json=True)
513
514     def make_curl_cmd(self, method, url, extra_args):
515         """Build curl command without executing."""
516         cmd = ["curl", "-i", "-s", "-X", method]
517         if self.get_http_insecure():
518             cmd.append("-k")
519         cmd.extend(["--connect-timeout", str(self.get_timeout_seconds())])
520         cmd.extend(["--header", "Accept: application/json"])
521         cmd.extend(["--header", "Content-Type: application/json"])
522         cmd.extend(["--header", "Authorization: Basic {0}".format(self.get_basic_credentials())])
523         cmd.extend(["--header", "X-ECOMP-InstanceID: {0}".format(Constants.ECOMP_INSTANCE_ID)])
524         cmd.extend(["--header", "X-ECOMP-RequestID: {0}".format(IRESTClient.new_uuid())])
525         if extra_args:
526             for extra_arg in extra_args:
527                 cmd.append(extra_arg)
528         cmd.append("{0}".format(url))
529         return cmd
530
531     @staticmethod
532     def debug_curl_cmd(cmd):
533         """Debug curl command, for diags and dryrun."""
534         buf = ""
535         for token in cmd:
536             if token is "curl" or token.startswith("-"):
537                 buf = "{0}{1} ".format(buf, token)
538             else:
539                 buf = "{0}\"{1}\" ".format(buf, token)
540         return buf
541
542     def _exec(self, method, extra_args=None, expect_json=None):
543         """Execute action.
544
545         Build command, invoke curl, validate and return response.
546         Overridden by DryRunRESTClient.
547         """
548         url = self.make_service_url()
549         cmd = self.make_curl_cmd(method, url, extra_args)
550         self.logger.info("Executing: {0}".format(CURLRESTClient.debug_curl_cmd(cmd)))
551
552         try:
553             output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode()
554             if not expect_json:
555                 return ResponseCodes.OK
556             try:
557                 separator = output.index("\r\n\r\n{")
558                 self.logger.debug("HTTP preamble:\n{0}".format(output[:separator]))
559                 json_body = json.loads(output[(separator+4):])
560                 self.log_json_response(method, json_body)
561                 return json_body
562             except ValueError:
563                 self.logger.warning("Couldn't find HTTP separator in curl output:\n{}".format(output))
564             code = CURLRESTClient.__get_http_code(output)
565             return IRESTClient._get_result_from_http_response(code)
566         except subprocess.CalledProcessError as err:
567             self.logger.exception(err)
568             return ResponseCodes.PROCESS_GENERAL_ERROR
569
570     @staticmethod
571     def __get_http_code(output):
572         """Attempt to guess HTTP result from (error) output."""
573         for line in output.splitlines():
574             if line.startswith("HTTP"):
575                 tokens = line.split()
576                 if len(tokens) > 2:
577                     try:
578                         return int(tokens[1])
579                     except ValueError:
580                         pass
581         return ResponseCodes.HTTP_GENERAL_ERROR
582
583
584 ###############################################################################
585
586
587 class DryRunRESTClient(CURLRESTClient):
588     """Neutered IRESTClient impl; only logs."""
589
590     def _exec(self, method, extra_args=None, expect_json=None):
591         """Override."""
592         url = self.make_service_url()
593         cmd = self.make_curl_cmd(method, url, extra_args)
594         self.logger.info("[DryRun] {0}".format(CURLRESTClient.debug_curl_cmd(cmd)))
595
596
597 ###############################################################################
598
599
600 class Runner(object):
601     """A bunch of static housekeeping supporting the launcher."""
602
603     @staticmethod
604     def get_logger():
605         """Get logger instance."""
606         return logging.getLogger(Constants.APPLICATION)
607
608     @staticmethod
609     def get_rest_client(args):
610         """Get the configured REST client impl, local, remote or dryrun."""
611         if args.dryrun:
612             return DryRunRESTClient(args)
613         elif args.curl:
614             return CURLRESTClient(args)
615         else:
616             return NativeRESTClient(args)
617
618     @staticmethod
619     def execute(args):
620         """Execute the requested action."""
621         client = Runner.get_rest_client(args)
622         if args.version:
623             print(Constants.VERSION)
624         elif args.help:
625             ArgumentParser.usage()
626         elif args.action == "list":
627             return client.list()
628         elif args.action == "create":
629             return client.create()
630         elif args.action == "update":
631             return client.update()
632         elif args.action == "delete":
633             return client.delete()
634         elif args.action == "checkout":
635             return client.version(FinalizeStatus.Checkout)
636         elif args.action == "checkin":
637             return client.version(FinalizeStatus.CheckIn)
638         elif args.action == "undocheckout":
639             return client.version(FinalizeStatus.UndoCheckout)
640         elif args.action == "submit":
641             return client.version(FinalizeStatus.Submit)
642         else:
643             logger = Runner.get_logger()
644             logger.info("No action specified. Try --help.")
645
646     @staticmethod
647     def parse_args(raw):
648         """Parse command-line args, returning dict."""
649         return ArgumentParser().parse_args(raw)
650
651
652 ###############################################################################
653
654
655 def execute(raw):
656     """Delegate which executes minus error-handling, exposed for unit-testing."""
657
658     # Intercept Python 2.X.
659
660     if not (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
661         raise EnvironmentError("Python 2.6/2.7 required")
662
663     # Parse command-line args.
664
665     args = Runner.parse_args(raw)
666
667     # Redirect logging to a file (freeing up STDIN) if directed.
668
669     logging.basicConfig(level=logging.INFO, filename=args.log, format=Constants.LOG_FORMAT)
670
671     # Set loglevel.
672
673     logger = Runner.get_logger()
674     if args.verbose:
675         logger.setLevel(logging.DEBUG)
676     logger.debug("Parsed arguments: {0}".format(args))
677
678     # Execute request.
679
680     return Runner.execute(args)
681
682
683 ###############################################################################
684
685
686 def main(raw):
687     """Execute for command-line arguments."""
688
689     logger = Runner.get_logger()
690     try:
691         result = execute(raw)
692         result_code = result if isinstance(result, int) else ResponseCodes.OK
693         logger.debug("Execution complete. Returning result {0} ({1})".format(result, result_code))
694         sys.exit(result_code)
695     except Exception as err:
696         logger.exception(err)
697         sys.exit(ResponseCodes.GENERAL_ERROR)
698
699
700 ###############################################################################
701
702
703 if __name__ == "__main__":
704     main(sys.argv[1:])