Change location of VES5.0 code
[demo.git] / vnfs / VES5.0 / evel / evel-test-collector / code / collector / collector.py
1 #!/usr/bin/env python
2 '''
3 Program which acts as the collector for the Vendor Event Listener REST API.
4
5 Only intended for test purposes.
6
7 License
8 -------
9
10 Copyright(c) <2016>, AT&T Intellectual Property.  All other rights reserved.
11
12 Redistribution and use in source and binary forms, with or without
13 modification, are permitted provided that the following conditions are met:
14
15 1. Redistributions of source code must retain the above copyright notice,
16    this list of conditions and the following disclaimer.
17 2. Redistributions in binary form must reproduce the above copyright notice,
18    this list of conditions and the following disclaimer in the documentation
19    and/or other materials provided with the distribution.
20 3. All advertising materials mentioning features or use of this software
21    must display the following acknowledgement:  This product includes
22    software developed by the AT&T.
23 4. Neither the name of AT&T nor the names of its contributors may be used to
24    endorse or promote products derived from this software without specific
25    prior written permission.
26
27 THIS SOFTWARE IS PROVIDED BY AT&T INTELLECTUAL PROPERTY ''AS IS'' AND ANY
28 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
29 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30 DISCLAIMED. IN NO EVENT SHALL AT&T INTELLECTUAL PROPERTY BE LIABLE FOR ANY
31 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
32 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
33 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
34 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
36 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 '''
38
39 from rest_dispatcher import PathDispatcher, set_404_content
40 from wsgiref.simple_server import make_server
41 import sys
42 import os
43 import platform
44 import traceback
45 import time
46 from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
47 import ConfigParser
48 import logging.handlers
49 from base64 import b64decode
50 import string
51 import json
52 import jsonschema
53 from functools import partial
54
55 _hello_resp = '''\
56 <html>
57   <head>
58      <title>Hello {name}</title>
59    </head>
60    <body>
61      <h1>Hello {name}!</h1>
62    </body>
63 </html>'''
64
65 _localtime_resp = '''\
66 <?xml version="1.0"?>
67 <time>
68   <year>{t.tm_year}</year>
69   <month>{t.tm_mon}</month>
70   <day>{t.tm_mday}</day>
71   <hour>{t.tm_hour}</hour>
72   <minute>{t.tm_min}</minute>
73   <second>{t.tm_sec}</second>
74 </time>'''
75
76 __all__ = []
77 __version__ = 0.1
78 __date__ = '2015-12-04'
79 __updated__ = '2015-12-04'
80
81 TESTRUN = False
82 DEBUG = False
83 PROFILE = False
84
85 #------------------------------------------------------------------------------
86 # Credentials we expect clients to authenticate themselves with.
87 #------------------------------------------------------------------------------
88 vel_username = ''
89 vel_password = ''
90
91 #------------------------------------------------------------------------------
92 # The JSON schema which we will use to validate events.
93 #------------------------------------------------------------------------------
94 vel_schema = None
95
96 #------------------------------------------------------------------------------
97 # The JSON schema which we will use to validate client throttle state.
98 #------------------------------------------------------------------------------
99 throttle_schema = None
100
101 #------------------------------------------------------------------------------
102 # The JSON schema which we will use to provoke throttling commands for testing.
103 #------------------------------------------------------------------------------
104 test_control_schema = None
105
106 #------------------------------------------------------------------------------
107 # Pending command list from the testControl API
108 # This is sent as a response commandList to the next received event.
109 #------------------------------------------------------------------------------
110 pending_command_list = None
111
112 #------------------------------------------------------------------------------
113 # Logger for this module.
114 #------------------------------------------------------------------------------
115 logger = None
116
117 def listener(environ, start_response, schema):
118     '''
119     Handler for the Vendor Event Listener REST API.
120
121     Extract headers and the body and check that:
122
123       1)  The client authenticated themselves correctly.
124       2)  The body validates against the provided schema for the API.
125
126     '''
127     logger.info('Got a Vendor Event request')
128     print('==== ' + time.asctime() + ' ' + '=' * 49)
129
130     #--------------------------------------------------------------------------
131     # Extract the content from the request.
132     #--------------------------------------------------------------------------
133     length = int(environ.get('CONTENT_LENGTH', '0'))
134     logger.debug('Content Length: {0}'.format(length))
135     body = environ['wsgi.input'].read(length)
136     logger.debug('Content Body: {0}'.format(body))
137
138     mode, b64_credentials = string.split(environ.get('HTTP_AUTHORIZATION',
139                                                      'None None'))
140     # logger.debug('Auth. Mode: {0} Credentials: {1}'.format(mode,
141     #                                                     b64_credentials))
142     logger.debug('Auth. Mode: {0} Credentials: ****'.format(mode))
143     if (b64_credentials != 'None'):
144         credentials = b64decode(b64_credentials)
145     else:
146         credentials = None
147
148     logger.debug('Credentials: {0}'.format(credentials))
149     #logger.debug('Credentials: ****')
150
151     #--------------------------------------------------------------------------
152     # If we have a schema file then check that the event matches that expected.
153     #--------------------------------------------------------------------------
154     if (schema is not None):
155         logger.debug('Attempting to validate data: {0}\n'
156                      'Against schema: {1}'.format(body, schema))
157         try:
158             decoded_body = json.loads(body)
159             jsonschema.validate(decoded_body, schema)
160             logger.info('Event is valid!')
161             print('Valid body decoded & checked against schema OK:\n'
162                   '{0}'.format(json.dumps(decoded_body,
163                                           sort_keys=True,
164                                           indent=4,
165                                           separators=(',', ': '))))
166
167         except jsonschema.SchemaError as e:
168             logger.error('Schema is not valid! {0}'.format(e))
169             print('Schema is not valid! {0}'.format(e))
170
171         except jsonschema.ValidationError as e:
172             logger.warn('Event is not valid against schema! {0}'.format(e))
173             print('Event is not valid against schema! {0}'.format(e))
174             print('Bad JSON body decoded:\n'
175                   '{0}'.format(json.dumps(decoded_body,
176                                          sort_keys=True,
177                                          indent=4,
178                                          separators=(',', ': '))))
179
180         except Exception as e:
181             logger.error('Event invalid for unexpected reason! {0}'.format(e))
182             print('Schema is not valid for unexpected reason! {0}'.format(e))
183     else:
184         logger.debug('No schema so just decode JSON: {0}'.format(body))
185         try:
186             decoded_body = json.loads(body)
187             print('Valid JSON body (no schema checking) decoded:\n'
188                   '{0}'.format(json.dumps(decoded_body,
189                                          sort_keys=True,
190                                          indent=4,
191                                          separators=(',', ': '))))
192             logger.info('Event is valid JSON but not checked against schema!')
193
194         except Exception as e:
195             logger.error('Event invalid for unexpected reason! {0}'.format(e))
196             print('JSON body not valid for unexpected reason! {0}'.format(e))
197
198     #--------------------------------------------------------------------------
199     # See whether the user authenticated themselves correctly.
200     #--------------------------------------------------------------------------
201     if (credentials == (vel_username + ':' + vel_password)):
202         logger.debug('Authenticated OK')
203         print('Authenticated OK')
204
205         #----------------------------------------------------------------------
206         # Respond to the caller. If we have a pending commandList from the
207         # testControl API, send it in response.
208         #----------------------------------------------------------------------
209         global pending_command_list
210         if pending_command_list is not None:
211             start_response('202 Accepted',
212                            [('Content-type', 'application/json')])
213             response = pending_command_list
214             pending_command_list = None
215
216             print('\n'+ '='*80)
217             print('Sending pending commandList in the response:\n'
218                   '{0}'.format(json.dumps(response,
219                                           sort_keys=True,
220                                           indent=4,
221                                           separators=(',', ': '))))
222             print('='*80 + '\n')
223             yield json.dumps(response)
224         else:
225             start_response('202 Accepted', [])
226             yield ''
227     else:
228         logger.warn('Failed to authenticate OK'+vel_username + ':' + vel_password)
229         print('Failed to authenticate OK'+vel_username + ':' + vel_password)
230
231         #----------------------------------------------------------------------
232         # Respond to the caller.
233         #----------------------------------------------------------------------
234         start_response('401 Unauthorized', [ ('Content-type',
235                                               'application/json')])
236         req_error = { 'requestError': {
237                         'policyException': {
238                             'messageId': 'POL0001',
239                             'text': 'Failed to authenticate'
240                             }
241                         }
242                     }
243         yield json.dumps(req_error)
244
245 def test_listener(environ, start_response, schema):
246     '''
247     Handler for the Test Collector Test Control API.
248
249     There is no authentication on this interface.
250
251     This simply stores a commandList which will be sent in response to the next
252     incoming event on the EVEL interface.
253     '''
254     global pending_command_list
255     logger.info('Got a Test Control input')
256     print('============================')
257     print('==== TEST CONTROL INPUT ====')
258
259     #--------------------------------------------------------------------------
260     # GET allows us to get the current pending request.
261     #--------------------------------------------------------------------------
262     if environ.get('REQUEST_METHOD') == 'GET':
263         start_response('200 OK', [('Content-type', 'application/json')])
264         yield json.dumps(pending_command_list)
265         return
266
267     #--------------------------------------------------------------------------
268     # Extract the content from the request.
269     #--------------------------------------------------------------------------
270     length = int(environ.get('CONTENT_LENGTH', '0'))
271     logger.debug('TestControl Content Length: {0}'.format(length))
272     body = environ['wsgi.input'].read(length)
273     logger.debug('TestControl Content Body: {0}'.format(body))
274
275     #--------------------------------------------------------------------------
276     # If we have a schema file then check that the event matches that expected.
277     #--------------------------------------------------------------------------
278     if (schema is not None):
279         logger.debug('Attempting to validate data: {0}\n'
280                      'Against schema: {1}'.format(body, schema))
281         try:
282             decoded_body = json.loads(body)
283             jsonschema.validate(decoded_body, schema)
284             logger.info('TestControl is valid!')
285             print('TestControl:\n'
286                   '{0}'.format(json.dumps(decoded_body,
287                                           sort_keys=True,
288                                           indent=4,
289                                           separators=(',', ': '))))
290
291         except jsonschema.SchemaError as e:
292             logger.error('TestControl Schema is not valid: {0}'.format(e))
293             print('TestControl Schema is not valid: {0}'.format(e))
294
295         except jsonschema.ValidationError as e:
296             logger.warn('TestControl input not valid: {0}'.format(e))
297             print('TestControl input not valid: {0}'.format(e))
298             print('Bad JSON body decoded:\n'
299                   '{0}'.format(json.dumps(decoded_body,
300                                           sort_keys=True,
301                                           indent=4,
302                                           separators=(',', ': '))))
303
304         except Exception as e:
305             logger.error('TestControl input not valid: {0}'.format(e))
306             print('TestControl Schema not valid: {0}'.format(e))
307     else:
308         logger.debug('Missing schema just decode JSON: {0}'.format(body))
309         try:
310             decoded_body = json.loads(body)
311             print('Valid JSON body (no schema checking) decoded:\n'
312                   '{0}'.format(json.dumps(decoded_body,
313                                           sort_keys=True,
314                                           indent=4,
315                                           separators=(',', ': '))))
316             logger.info('TestControl input not checked against schema!')
317
318         except Exception as e:
319             logger.error('TestControl input not valid: {0}'.format(e))
320             print('TestControl input not valid: {0}'.format(e))
321
322     #--------------------------------------------------------------------------
323     # Respond to the caller. If we received otherField 'ThrottleRequest',
324     # generate the appropriate canned response.
325     #--------------------------------------------------------------------------
326     pending_command_list = decoded_body
327     print('===== TEST CONTROL END =====')
328     print('============================')
329     start_response('202 Accepted', [])
330     yield ''
331
332 def main(argv=None):
333     '''
334     Main function for the collector start-up.
335
336     Called with command-line arguments:
337         *    --config *<file>*
338         *    --section *<section>*
339         *    --verbose
340
341     Where:
342
343         *<file>* specifies the path to the configuration file.
344
345         *<section>* specifies the section within that config file.
346
347         *verbose* generates more information in the log files.
348
349     The process listens for REST API invocations and checks them. Errors are
350     displayed to stdout and logged.
351     '''
352
353     if argv is None:
354         argv = sys.argv
355     else:
356         sys.argv.extend(argv)
357
358     program_name = os.path.basename(sys.argv[0])
359     program_version = 'v{0}'.format(__version__)
360     program_build_date = str(__updated__)
361     program_version_message = '%%(prog)s {0} ({1})'.format(program_version,
362                                                          program_build_date)
363     if (__import__('__main__').__doc__ is not None):
364         program_shortdesc = __import__('__main__').__doc__.split('\n')[1]
365     else:
366         program_shortdesc = 'Running in test harness'
367     program_license = '''{0}
368
369   Created  on {1}.
370   Copyright 2015 Metaswitch Networks Ltd. All rights reserved.
371
372   Distributed on an "AS IS" basis without warranties
373   or conditions of any kind, either express or implied.
374
375 USAGE
376 '''.format(program_shortdesc, str(__date__))
377
378     try:
379         #----------------------------------------------------------------------
380         # Setup argument parser so we can parse the command-line.
381         #----------------------------------------------------------------------
382         parser = ArgumentParser(description=program_license,
383                                 formatter_class=ArgumentDefaultsHelpFormatter)
384         parser.add_argument('-v', '--verbose',
385                             dest='verbose',
386                             action='count',
387                             help='set verbosity level')
388         parser.add_argument('-V', '--version',
389                             action='version',
390                             version=program_version_message,
391                             help='Display version information')
392         parser.add_argument('-a', '--api-version',
393                             dest='api_version',
394                             default='5',
395                             help='set API version')
396         parser.add_argument('-c', '--config',
397                             dest='config',
398                             default='/etc/opt/att/collector.conf',
399                             help='Use this config file.',
400                             metavar='<file>')
401         parser.add_argument('-s', '--section',
402                             dest='section',
403                             default='default',
404                             metavar='<section>',
405                             help='section to use in the config file')
406
407         #----------------------------------------------------------------------
408         # Process arguments received.
409         #----------------------------------------------------------------------
410         args = parser.parse_args()
411         verbose = args.verbose
412         api_version = args.api_version
413         config_file = args.config
414         config_section = args.section
415
416         #----------------------------------------------------------------------
417         # Now read the config file, using command-line supplied values as
418         # overrides.
419         #----------------------------------------------------------------------
420         defaults = {'log_file': 'collector.log',
421                     'vel_port': '12233',
422                     'vel_path': '',
423                     'vel_topic_name': ''
424                    }
425         overrides = {}
426         config = ConfigParser.SafeConfigParser(defaults)
427         config.read(config_file)
428
429         #----------------------------------------------------------------------
430         # extract the values we want.
431         #----------------------------------------------------------------------
432         log_file = config.get(config_section, 'log_file', vars=overrides)
433         vel_port = config.get(config_section, 'vel_port', vars=overrides)
434         vel_path = config.get(config_section, 'vel_path', vars=overrides)
435         vel_topic_name = config.get(config_section,
436                                     'vel_topic_name',
437                                     vars=overrides)
438         global vel_username
439         global vel_password
440         vel_username = config.get(config_section,
441                                   'vel_username',
442                                   vars=overrides)
443         vel_password = config.get(config_section,
444                                   'vel_password',
445                                   vars=overrides)
446         vel_schema_file = config.get(config_section,
447                                      'schema_file',
448                                      vars=overrides)
449         base_schema_file = config.get(config_section,
450                                       'base_schema_file',
451                                       vars=overrides)
452         throttle_schema_file = config.get(config_section,
453                                           'throttle_schema_file',
454                                           vars=overrides)
455         test_control_schema_file = config.get(config_section,
456                                            'test_control_schema_file',
457                                            vars=overrides)
458
459         #----------------------------------------------------------------------
460         # Finally we have enough info to start a proper flow trace.
461         #----------------------------------------------------------------------
462         global logger
463         print('Logfile: {0}'.format(log_file))
464         logger = logging.getLogger('collector')
465         if verbose > 0:
466             print('Verbose mode on')
467             logger.setLevel(logging.DEBUG)
468         else:
469             logger.setLevel(logging.INFO)
470         handler = logging.handlers.RotatingFileHandler(log_file,
471                                                        maxBytes=1000000,
472                                                        backupCount=10)
473         if (platform.system() == 'Windows'):
474             date_format = '%Y-%m-%d %H:%M:%S'
475         else:
476             date_format = '%Y-%m-%d %H:%M:%S.%f %z'
477         formatter = logging.Formatter('%(asctime)s %(name)s - '
478                                       '%(levelname)s - %(message)s',
479                                       date_format)
480         handler.setFormatter(formatter)
481         logger.addHandler(handler)
482         logger.info('Started')
483
484         #----------------------------------------------------------------------
485         # Log the details of the configuration.
486         #----------------------------------------------------------------------
487         logger.debug('Log file = {0}'.format(log_file))
488         logger.debug('Event Listener Port = {0}'.format(vel_port))
489         logger.debug('Event Listener Path = {0}'.format(vel_path))
490         logger.debug('Event Listener Topic = {0}'.format(vel_topic_name))
491         logger.debug('Event Listener Username = {0}'.format(vel_username))
492         # logger.debug('Event Listener Password = {0}'.format(vel_password))
493         logger.debug('Event Listener JSON Schema File = {0}'.format(
494                                                               vel_schema_file))
495         logger.debug('Base JSON Schema File = {0}'.format(base_schema_file))
496         logger.debug('Throttle JSON Schema File = {0}'.format(
497                                                          throttle_schema_file))
498         logger.debug('Test Control JSON Schema File = {0}'.format(
499                                                      test_control_schema_file))
500
501         #----------------------------------------------------------------------
502         # Perform some basic error checking on the config.
503         #----------------------------------------------------------------------
504         if (int(vel_port) < 1024 or int(vel_port) > 65535):
505             logger.error('Invalid Vendor Event Listener port ({0}) '
506                          'specified'.format(vel_port))
507             raise RuntimeError('Invalid Vendor Event Listener port ({0}) '
508                                'specified'.format(vel_port))
509
510         if (len(vel_path) > 0 and vel_path[-1] != '/'):
511             logger.warning('Event Listener Path ({0}) should have terminating '
512                            '"/"!  Adding one on to configured string.'.format(
513                                                                      vel_path))
514             vel_path += '/'
515
516         #----------------------------------------------------------------------
517         # Load up the vel_schema, if it exists.
518         #----------------------------------------------------------------------
519         if not os.path.exists(vel_schema_file):
520             logger.warning('Event Listener Schema File ({0}) not found. '
521                            'No validation will be undertaken.'.format(
522                                                               vel_schema_file))
523         else:
524             global vel_schema
525             global throttle_schema
526             global test_control_schema
527             vel_schema = json.load(open(vel_schema_file, 'r'))
528             logger.debug('Loaded the JSON schema file')
529
530             #------------------------------------------------------------------
531             # Load up the throttle_schema, if it exists.
532             #------------------------------------------------------------------
533             if (os.path.exists(throttle_schema_file)):
534                 logger.debug('Loading throttle schema')
535                 throttle_fragment = json.load(open(throttle_schema_file, 'r'))
536                 throttle_schema = {}
537                 throttle_schema.update(vel_schema)
538                 throttle_schema.update(throttle_fragment)
539                 logger.debug('Loaded the throttle schema')
540
541             #------------------------------------------------------------------
542             # Load up the test control _schema, if it exists.
543             #------------------------------------------------------------------
544             if (os.path.exists(test_control_schema_file)):
545                 logger.debug('Loading test control schema')
546                 test_control_fragment = json.load(
547                     open(test_control_schema_file, 'r'))
548                 test_control_schema = {}
549                 test_control_schema.update(vel_schema)
550                 test_control_schema.update(test_control_fragment)
551                 logger.debug('Loaded the test control schema')
552
553             #------------------------------------------------------------------
554             # Load up the base_schema, if it exists.
555             #------------------------------------------------------------------
556             if (os.path.exists(base_schema_file)):
557                 logger.debug('Updating the schema with base definition')
558                 base_schema = json.load(open(base_schema_file, 'r'))
559                 vel_schema.update(base_schema)
560                 logger.debug('Updated the JSON schema file')
561
562         #----------------------------------------------------------------------
563         # We are now ready to get started with processing. Start-up the various
564         # components of the system in order:
565         #
566         #  1) Create the dispatcher.
567         #  2) Register the functions for the URLs of interest.
568         #  3) Run the webserver.
569         #----------------------------------------------------------------------
570         root_url = '/{0}eventListener/v{1}{2}'.\
571                    format(vel_path,
572                           api_version,
573                           '/' + vel_topic_name
574                           if len(vel_topic_name) > 0
575                           else '')
576         throttle_url = '/{0}eventListener/v{1}/clientThrottlingState'.\
577                        format(vel_path, api_version)
578         set_404_content(root_url)
579         dispatcher = PathDispatcher()
580         vendor_event_listener = partial(listener, schema = vel_schema)
581         dispatcher.register('GET', root_url, vendor_event_listener)
582         dispatcher.register('POST', root_url, vendor_event_listener)
583         vendor_throttle_listener = partial(listener, schema = throttle_schema)
584         dispatcher.register('GET', throttle_url, vendor_throttle_listener)
585         dispatcher.register('POST', throttle_url, vendor_throttle_listener)
586
587         #----------------------------------------------------------------------
588         # We also add a POST-only mechanism for test control, so that we can
589         # send commands to a single attached client.
590         #----------------------------------------------------------------------
591         test_control_url = '/testControl/v{0}/commandList'.format(api_version)
592         test_control_listener = partial(test_listener,
593                                         schema = test_control_schema)
594         dispatcher.register('POST', test_control_url, test_control_listener)
595         dispatcher.register('GET', test_control_url, test_control_listener)
596
597         httpd = make_server('', int(vel_port), dispatcher)
598         print('Serving on port {0}...'.format(vel_port))
599         httpd.serve_forever()
600
601         logger.error('Main loop exited unexpectedly!')
602         return 0
603
604     except KeyboardInterrupt:
605         #----------------------------------------------------------------------
606         # handle keyboard interrupt
607         #----------------------------------------------------------------------
608         logger.info('Exiting on keyboard interrupt!')
609         return 0
610
611     except Exception as e:
612         #----------------------------------------------------------------------
613         # Handle unexpected exceptions.
614         #----------------------------------------------------------------------
615         if DEBUG or TESTRUN:
616             raise(e)
617         indent = len(program_name) * ' '
618         sys.stderr.write(program_name + ': ' + repr(e) + '\n')
619         sys.stderr.write(indent + '  for help use --help\n')
620         sys.stderr.write(traceback.format_exc())
621         logger.critical('Exiting because of exception: {0}'.format(e))
622         logger.critical(traceback.format_exc())
623         return 2
624
625 #------------------------------------------------------------------------------
626 # MAIN SCRIPT ENTRY POINT.
627 #------------------------------------------------------------------------------
628 if __name__ == '__main__':
629     if TESTRUN:
630         #----------------------------------------------------------------------
631         # Running tests - note that doctest comments haven't been included so
632         # this is a hook for future improvements.
633         #----------------------------------------------------------------------
634         import doctest
635         doctest.testmod()
636
637     if PROFILE:
638         #----------------------------------------------------------------------
639         # Profiling performance.  Performance isn't expected to be a major
640         # issue, but this should all work as expected.
641         #----------------------------------------------------------------------
642         import cProfile
643         import pstats
644         profile_filename = 'collector_profile.txt'
645         cProfile.run('main()', profile_filename)
646         statsfile = open('collector_profile_stats.txt', 'wb')
647         p = pstats.Stats(profile_filename, stream=statsfile)
648         stats = p.strip_dirs().sort_stats('cumulative')
649         stats.print_stats()
650         statsfile.close()
651         sys.exit(0)
652
653     #--------------------------------------------------------------------------
654     # Normal operation - call through to the main function.
655     #--------------------------------------------------------------------------
656     sys.exit(main())