The keystoneclient.openstack.common is error path of exception.
[escalator.git] / client / escalatorclient / shell.py
1 # Copyright 2012 OpenStack Foundation
2 # All Rights Reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15
16 """
17 Command-line interface to the OpenStack Images API.
18 """
19
20 from __future__ import print_function
21
22 import argparse
23 import copy
24 import getpass
25 import json
26 import logging
27 import os
28 from os.path import expanduser
29 import sys
30 import traceback
31
32 from oslo_utils import encodeutils
33 from oslo_utils import importutils
34 import six.moves.urllib.parse as urlparse
35
36 import escalatorclient
37 from escalatorclient import _i18n
38 from escalatorclient.common import utils
39 from escalatorclient import exc
40
41 from keystoneclient.auth.identity import v2 as v2_auth
42 from keystoneclient.auth.identity import v3 as v3_auth
43 from keystoneclient import discover
44 from keystoneclient import exceptions as ks_exc
45 from keystoneclient import session
46
47 osprofiler_profiler = importutils.try_import("osprofiler.profiler")
48 _ = _i18n._
49
50
51 class escalatorShell(object):
52
53     def _append_global_identity_args(self, parser):
54         # FIXME(bobt): these are global identity (Keystone) arguments which
55         # should be consistent and shared by all service clients. Therefore,
56         # they should be provided by python-keystoneclient. We will need to
57         # refactor this code once this functionality is avaible in
58         # python-keystoneclient. See
59         #
60         # https://bugs.launchpad.net/python-keystoneclient/+bug/1332337
61         #
62         parser.add_argument('-k', '--insecure',
63                             default=False,
64                             action='store_true',
65                             help='Explicitly allow escalatorclient to perform '
66                             '\"insecure SSL\" (https) requests. The server\'s '
67                             'certificate will not be verified against any '
68                             'certificate authorities. This option should '
69                             'be used with caution.')
70
71         parser.add_argument('--os-cert',
72                             help='Path of certificate file to use in SSL '
73                             'connection. This file can optionally be '
74                             'prepended with the private key.')
75
76         parser.add_argument('--cert-file',
77                             dest='os_cert',
78                             help='DEPRECATED! Use --os-cert.')
79
80         parser.add_argument('--os-key',
81                             help='Path of client key to use in SSL '
82                             'connection. This option is not necessary '
83                             'if your key is prepended to your cert file.')
84
85         parser.add_argument('--key-file',
86                             dest='os_key',
87                             help='DEPRECATED! Use --os-key.')
88
89         parser.add_argument('--os-cacert',
90                             metavar='<ca-certificate-file>',
91                             dest='os_cacert',
92                             default=utils.env('OS_CACERT'),
93                             help='Path of CA TLS certificate(s) used to '
94                             'verify the remote server\'s certificate. '
95                             'Without this option escalator looks for the '
96                             'default system CA certificates.')
97
98         parser.add_argument('--ca-file',
99                             dest='os_cacert',
100                             help='DEPRECATED! Use --os-cacert.')
101
102         parser.add_argument('--os-username',
103                             default=utils.env('OS_USERNAME'),
104                             help='Defaults to env[OS_USERNAME].')
105
106         parser.add_argument('--os_username',
107                             help=argparse.SUPPRESS)
108
109         parser.add_argument('--os-user-id',
110                             default=utils.env('OS_USER_ID'),
111                             help='Defaults to env[OS_USER_ID].')
112
113         parser.add_argument('--os-user-domain-id',
114                             default=utils.env('OS_USER_DOMAIN_ID'),
115                             help='Defaults to env[OS_USER_DOMAIN_ID].')
116
117         parser.add_argument('--os-user-domain-name',
118                             default=utils.env('OS_USER_DOMAIN_NAME'),
119                             help='Defaults to env[OS_USER_DOMAIN_NAME].')
120
121         parser.add_argument('--os-project-id',
122                             default=utils.env('OS_PROJECT_ID'),
123                             help='Another way to specify tenant ID. '
124                                  'This option is mutually exclusive with '
125                                  ' --os-tenant-id. '
126                                  'Defaults to env[OS_PROJECT_ID].')
127
128         parser.add_argument('--os-project-name',
129                             default=utils.env('OS_PROJECT_NAME'),
130                             help='Another way to specify tenant name. '
131                                  'This option is mutually exclusive with '
132                                  ' --os-tenant-name. '
133                                  'Defaults to env[OS_PROJECT_NAME].')
134
135         parser.add_argument('--os-project-domain-id',
136                             default=utils.env('OS_PROJECT_DOMAIN_ID'),
137                             help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
138
139         parser.add_argument('--os-project-domain-name',
140                             default=utils.env('OS_PROJECT_DOMAIN_NAME'),
141                             help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
142
143         parser.add_argument('--os-password',
144                             default=utils.env('OS_PASSWORD'),
145                             help='Defaults to env[OS_PASSWORD].')
146
147         parser.add_argument('--os_password',
148                             help=argparse.SUPPRESS)
149
150         parser.add_argument('--os-tenant-id',
151                             default=utils.env('OS_TENANT_ID'),
152                             help='Defaults to env[OS_TENANT_ID].')
153
154         parser.add_argument('--os_tenant_id',
155                             help=argparse.SUPPRESS)
156
157         parser.add_argument('--os-tenant-name',
158                             default=utils.env('OS_TENANT_NAME'),
159                             help='Defaults to env[OS_TENANT_NAME].')
160
161         parser.add_argument('--os_tenant_name',
162                             help=argparse.SUPPRESS)
163
164         parser.add_argument('--os-auth-url',
165                             default=utils.env('OS_AUTH_URL'),
166                             help='Defaults to env[OS_AUTH_URL].')
167
168         parser.add_argument('--os_auth_url',
169                             help=argparse.SUPPRESS)
170
171         parser.add_argument('--os-region-name',
172                             default=utils.env('OS_REGION_NAME'),
173                             help='Defaults to env[OS_REGION_NAME].')
174
175         parser.add_argument('--os_region_name',
176                             help=argparse.SUPPRESS)
177
178         parser.add_argument('--os-auth-token',
179                             default=utils.env('OS_AUTH_TOKEN'),
180                             help='Defaults to env[OS_AUTH_TOKEN].')
181
182         parser.add_argument('--os_auth_token',
183                             help=argparse.SUPPRESS)
184
185         parser.add_argument('--os-service-type',
186                             default=utils.env('OS_SERVICE_TYPE'),
187                             help='Defaults to env[OS_SERVICE_TYPE].')
188
189         parser.add_argument('--os_service_type',
190                             help=argparse.SUPPRESS)
191
192         parser.add_argument('--os-endpoint-type',
193                             default=utils.env('OS_ENDPOINT_TYPE'),
194                             help='Defaults to env[OS_ENDPOINT_TYPE].')
195
196         parser.add_argument('--os_endpoint_type',
197                             help=argparse.SUPPRESS)
198
199         parser.add_argument('--os-endpoint',
200                             default=utils.env('OS_ENDPOINT'),
201                             help='Defaults to env[OS_ENDPOINT].')
202
203         parser.add_argument('--os_endpoint',
204                             help=argparse.SUPPRESS)
205
206     def get_base_parser(self):
207         parser = argparse.ArgumentParser(
208             prog='escalator',
209             description=__doc__.strip(),
210             epilog='See "escalator help COMMAND" '
211                    'for help on a specific command.',
212             add_help=False,
213             formatter_class=HelpFormatter,
214         )
215
216         # Global arguments
217         parser.add_argument('-h', '--help',
218                             action='store_true',
219                             help=argparse.SUPPRESS,
220                             )
221
222         parser.add_argument('-d', '--debug',
223                             default=bool(utils.env('ESCALATORCLIENT_DEBUG')),
224                             action='store_true',
225                             help='Defaults to env[ESCALATORCLIENT_DEBUG].')
226
227         parser.add_argument('-v', '--verbose',
228                             default=False, action="store_true",
229                             help="Print more verbose output")
230
231         parser.add_argument('--get-schema',
232                             default=False, action="store_true",
233                             dest='get_schema',
234                             help='Ignores cached copy and forces retrieval '
235                                  'of schema that generates portions of the '
236                                  'help text. Ignored with API version 1.')
237
238         parser.add_argument('--timeout',
239                             default=600,
240                             help='Number of seconds to wait for a response')
241
242         parser.add_argument('--no-ssl-compression',
243                             dest='ssl_compression',
244                             default=True, action='store_false',
245                             help='Disable SSL compression when using https.')
246
247         parser.add_argument('-f', '--force',
248                             dest='force',
249                             default=False, action='store_true',
250                             help='Prevent select actions from requesting '
251                             'user confirmation.')
252
253         parser.add_argument('--os-image-url',
254                             default=utils.env('OS_IMAGE_URL'),
255                             help=('Defaults to env[OS_IMAGE_URL]. '
256                                   'If the provided image url contains '
257                                   'a version number and '
258                                   '`--os-image-api-version` is omitted '
259                                   'the version of the URL will be picked as '
260                                   'the image api version to use.'))
261
262         parser.add_argument('--os_image_url',
263                             help=argparse.SUPPRESS)
264
265         parser.add_argument('--os-image-api-version',
266                             default=utils.env('OS_IMAGE_API_VERSION',
267                                               default=None),
268                             help='Defaults to env[OS_IMAGE_API_VERSION] or 1.')
269
270         parser.add_argument('--os_image_api_version',
271                             help=argparse.SUPPRESS)
272
273         if osprofiler_profiler:
274             parser.add_argument('--profile',
275                                 metavar='HMAC_KEY',
276                                 help='HMAC key to use for encrypting context '
277                                 'data for performance profiling of operation. '
278                                 'This key should be the value of HMAC key '
279                                 'configured in osprofiler middleware in '
280                                 'escalator, it is specified in paste '
281                                 'configuration file at '
282                                 '/etc/escalator/api-paste.ini and '
283                                 '/etc/escalator/registry-paste.ini. '
284                                 'Without key '
285                                 'the profiling will not be triggered even '
286                                 'if osprofiler is enabled on server side.')
287
288         # FIXME(bobt): this method should come from python-keystoneclient
289         self._append_global_identity_args(parser)
290
291         return parser
292
293     def get_subcommand_parser(self, version):
294         parser = self.get_base_parser()
295
296         self.subcommands = {}
297         subparsers = parser.add_subparsers(metavar='<subcommand>')
298         try:
299             submodule = utils.import_versioned_module(version, 'shell')
300         except ImportError:
301             print('"%s" is not a supported API version. Example '
302                   'values are "1" or "2".' % version)
303             utils.exit()
304
305         self._find_actions(subparsers, submodule)
306         self._find_actions(subparsers, self)
307
308         self._add_bash_completion_subparser(subparsers)
309
310         return parser
311
312     def _find_actions(self, subparsers, actions_module):
313         for attr in (a for a in dir(actions_module) if a.startswith('do_')):
314             # I prefer to be hypen-separated instead of underscores.
315             command = attr[3:].replace('_', '-')
316             callback = getattr(actions_module, attr)
317             desc = callback.__doc__ or ''
318             help = desc.strip().split('\n')[0]
319             arguments = getattr(callback, 'arguments', [])
320
321             subparser = subparsers.add_parser(command,
322                                               help=help,
323                                               description=desc,
324                                               add_help=False,
325                                               formatter_class=HelpFormatter
326                                               )
327             subparser.add_argument('-h', '--help',
328                                    action='help',
329                                    help=argparse.SUPPRESS,
330                                    )
331             self.subcommands[command] = subparser
332             for (args, kwargs) in arguments:
333                 subparser.add_argument(*args, **kwargs)
334             subparser.set_defaults(func=callback)
335
336     def _add_bash_completion_subparser(self, subparsers):
337         subparser = subparsers.add_parser('bash_completion',
338                                           add_help=False,
339                                           formatter_class=HelpFormatter)
340         self.subcommands['bash_completion'] = subparser
341         subparser.set_defaults(func=self.do_bash_completion)
342
343     def _get_image_url(self, args):
344         """Translate the available url-related options into a single string.
345
346         Return the endpoint that should be used to talk to escalator if a
347         clear decision can be made. Otherwise, return None.
348         """
349         if args.os_image_url:
350             return args.os_image_url
351         else:
352             return None
353
354     def _discover_auth_versions(self, session, auth_url):
355         # discover the API versions the server is supporting base on the
356         # given URL
357         v2_auth_url = None
358         v3_auth_url = None
359         try:
360             ks_discover = discover.Discover(session=session, auth_url=auth_url)
361             v2_auth_url = ks_discover.url_for('2.0')
362             v3_auth_url = ks_discover.url_for('3.0')
363         except ks_exc.ClientException as e:
364             # Identity service may not support discover API version.
365             # Lets trying to figure out the API version from the original URL.
366             url_parts = urlparse.urlparse(auth_url)
367             (scheme, netloc, path, params, query, fragment) = url_parts
368             path = path.lower()
369             if path.startswith('/v3'):
370                 v3_auth_url = auth_url
371             elif path.startswith('/v2'):
372                 v2_auth_url = auth_url
373             else:
374                 # not enough information to determine the auth version
375                 msg = ('Unable to determine the Keystone version '
376                        'to authenticate with using the given '
377                        'auth_url. Identity service may not support API '
378                        'version discovery. Please provide a versioned '
379                        'auth_url instead. error=%s') % (e)
380                 raise exc.CommandError(msg)
381
382         return (v2_auth_url, v3_auth_url)
383
384     def _get_keystone_session(self, **kwargs):
385         ks_session = session.Session.construct(kwargs)
386
387         # discover the supported keystone versions using the given auth url
388         auth_url = kwargs.pop('auth_url', None)
389         (v2_auth_url, v3_auth_url) = self._discover_auth_versions(
390             session=ks_session,
391             auth_url=auth_url)
392
393         # Determine which authentication plugin to use. First inspect the
394         # auth_url to see the supported version. If both v3 and v2 are
395         # supported, then use the highest version if possible.
396         user_id = kwargs.pop('user_id', None)
397         username = kwargs.pop('username', None)
398         password = kwargs.pop('password', None)
399         user_domain_name = kwargs.pop('user_domain_name', None)
400         user_domain_id = kwargs.pop('user_domain_id', None)
401         # project and tenant can be used interchangeably
402         project_id = (kwargs.pop('project_id', None) or
403                       kwargs.pop('tenant_id', None))
404         project_name = (kwargs.pop('project_name', None) or
405                         kwargs.pop('tenant_name', None))
406         project_domain_id = kwargs.pop('project_domain_id', None)
407         project_domain_name = kwargs.pop('project_domain_name', None)
408         auth = None
409
410         use_domain = (user_domain_id or
411                       user_domain_name or
412                       project_domain_id or
413                       project_domain_name)
414         use_v3 = v3_auth_url and (use_domain or (not v2_auth_url))
415         use_v2 = v2_auth_url and not use_domain
416
417         if use_v3:
418             auth = v3_auth.Password(
419                 v3_auth_url,
420                 user_id=user_id,
421                 username=username,
422                 password=password,
423                 user_domain_id=user_domain_id,
424                 user_domain_name=user_domain_name,
425                 project_id=project_id,
426                 project_name=project_name,
427                 project_domain_id=project_domain_id,
428                 project_domain_name=project_domain_name)
429         elif use_v2:
430             auth = v2_auth.Password(
431                 v2_auth_url,
432                 username,
433                 password,
434                 tenant_id=project_id,
435                 tenant_name=project_name)
436         else:
437             # if we get here it means domain information is provided
438             # (caller meant to use Keystone V3) but the auth url is
439             # actually Keystone V2. Obviously we can't authenticate a V3
440             # user using V2.
441             exc.CommandError("Credential and auth_url mismatch. The given "
442                              "auth_url is using Keystone V2 endpoint, which "
443                              "may not able to handle Keystone V3 credentials. "
444                              "Please provide a correct Keystone V3 auth_url.")
445
446         ks_session.auth = auth
447         return ks_session
448
449     def _get_endpoint_and_token(self, args, force_auth=False):
450         image_url = self._get_image_url(args)
451         auth_token = args.os_auth_token
452
453         auth_reqd = force_auth or\
454             (utils.is_authentication_required(args.func) and not
455              (auth_token and image_url))
456
457         if not auth_reqd:
458             endpoint = image_url
459             token = args.os_auth_token
460         else:
461
462             if not args.os_username:
463                 raise exc.CommandError(
464                     _("You must provide a username via"
465                       " either --os-username or "
466                       "env[OS_USERNAME]"))
467
468             if not args.os_password:
469                 # No password, If we've got a tty, try prompting for it
470                 if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
471                     # Check for Ctl-D
472                     try:
473                         args.os_password = getpass.getpass('OS Password: ')
474                     except EOFError:
475                         pass
476                 # No password because we didn't have a tty or the
477                 # user Ctl-D when prompted.
478                 if not args.os_password:
479                     raise exc.CommandError(
480                         _("You must provide a password via "
481                           "either --os-password, "
482                           "env[OS_PASSWORD], "
483                           "or prompted response"))
484
485             # Validate password flow auth
486             project_info = (
487                 args.os_tenant_name or args.os_tenant_id or (
488                     args.os_project_name and (
489                         args.os_project_domain_name or
490                         args.os_project_domain_id
491                     )
492                 ) or args.os_project_id
493             )
494
495             if not project_info:
496                 # tenant is deprecated in Keystone v3. Use the latest
497                 # terminology instead.
498                 raise exc.CommandError(
499                     _("You must provide a project_id or project_name ("
500                       "with project_domain_name or project_domain_id) "
501                       "via "
502                       "  --os-project-id (env[OS_PROJECT_ID])"
503                       "  --os-project-name (env[OS_PROJECT_NAME]),"
504                       "  --os-project-domain-id "
505                       "(env[OS_PROJECT_DOMAIN_ID])"
506                       "  --os-project-domain-name "
507                       "(env[OS_PROJECT_DOMAIN_NAME])"))
508
509             if not args.os_auth_url:
510                 raise exc.CommandError(
511                     _("You must provide an auth url via"
512                       " either --os-auth-url or "
513                       "via env[OS_AUTH_URL]"))
514
515             kwargs = {
516                 'auth_url': args.os_auth_url,
517                 'username': args.os_username,
518                 'user_id': args.os_user_id,
519                 'user_domain_id': args.os_user_domain_id,
520                 'user_domain_name': args.os_user_domain_name,
521                 'password': args.os_password,
522                 'tenant_name': args.os_tenant_name,
523                 'tenant_id': args.os_tenant_id,
524                 'project_name': args.os_project_name,
525                 'project_id': args.os_project_id,
526                 'project_domain_name': args.os_project_domain_name,
527                 'project_domain_id': args.os_project_domain_id,
528                 'insecure': args.insecure,
529                 'cacert': args.os_cacert,
530                 'cert': args.os_cert,
531                 'key': args.os_key
532             }
533             ks_session = self._get_keystone_session(**kwargs)
534             token = args.os_auth_token or ks_session.get_token()
535
536             endpoint_type = args.os_endpoint_type or 'public'
537             service_type = args.os_service_type or 'image'
538             endpoint = args.os_image_url or ks_session.get_endpoint(
539                 service_type=service_type,
540                 interface=endpoint_type,
541                 region_name=args.os_region_name)
542
543         return endpoint, token
544
545     def _get_versioned_client(self, api_version, args, force_auth=False):
546         # ndpoint, token = self._get_endpoint_and_token(
547         # args,force_auth=force_auth)
548         # endpoint = "http://10.43.175.62:19292"
549         endpoint = args.os_endpoint
550         # print endpoint
551         kwargs = {
552             # 'token': token,
553             'insecure': args.insecure,
554             'timeout': args.timeout,
555             'cacert': args.os_cacert,
556             'cert': args.os_cert,
557             'key': args.os_key,
558             'ssl_compression': args.ssl_compression
559         }
560         client = escalatorclient.Client(api_version, endpoint, **kwargs)
561         return client
562
563     def _cache_schemas(self, options, home_dir='~/.escalatorclient'):
564         homedir = expanduser(home_dir)
565         if not os.path.exists(homedir):
566             os.makedirs(homedir)
567
568         resources = ['image', 'metadefs/namespace', 'metadefs/resource_type']
569         schema_file_paths = [homedir + os.sep + x + '_schema.json'
570                              for x in ['image', 'namespace', 'resource_type']]
571
572         client = None
573         for resource, schema_file_path in zip(resources, schema_file_paths):
574             if (not os.path.exists(schema_file_path)) or options.get_schema:
575                 try:
576                     if not client:
577                         client = self._get_versioned_client('2', options,
578                                                             force_auth=True)
579                     schema = client.schemas.get(resource)
580
581                     with open(schema_file_path, 'w') as f:
582                         f.write(json.dumps(schema.raw()))
583                 except Exception:
584                     # NOTE(esheffield) do nothing here, we'll get a message
585                     # later if the schema is missing
586                     pass
587
588     def main(self, argv):
589         # Parse args once to find version
590
591         # NOTE(flepied) Under Python3, parsed arguments are removed
592         # from the list so make a copy for the first parsing
593         base_argv = copy.deepcopy(argv)
594         parser = self.get_base_parser()
595         (options, args) = parser.parse_known_args(base_argv)
596
597         try:
598             # NOTE(flaper87): Try to get the version from the
599             # image-url first. If no version was specified, fallback
600             # to the api-image-version arg. If both of these fail then
601             # fallback to the minimum supported one and let keystone
602             # do the magic.
603             endpoint = self._get_image_url(options)
604             endpoint, url_version = utils.strip_version(endpoint)
605         except ValueError:
606             # NOTE(flaper87): ValueError is raised if no endpoint is povided
607             url_version = None
608
609         # build available subcommands based on version
610         try:
611             api_version = int(options.os_image_api_version or url_version or 1)
612         except ValueError:
613             print("Invalid API version parameter")
614             utils.exit()
615
616         if api_version == 2:
617             self._cache_schemas(options)
618
619         subcommand_parser = self.get_subcommand_parser(api_version)
620         self.parser = subcommand_parser
621
622         # Handle top-level --help/-h before attempting to parse
623         # a command off the command line
624         if options.help or not argv:
625             self.do_help(options)
626             return 0
627
628         # Parse args again and call whatever callback was selected
629         args = subcommand_parser.parse_args(argv)
630
631         # Short-circuit and deal with help command right away.
632         if args.func == self.do_help:
633             self.do_help(args)
634             return 0
635         elif args.func == self.do_bash_completion:
636             self.do_bash_completion(args)
637             return 0
638
639         LOG = logging.getLogger('escalatorclient')
640         LOG.addHandler(logging.StreamHandler())
641         LOG.setLevel(logging.DEBUG if args.debug else logging.INFO)
642
643         profile = osprofiler_profiler and options.profile
644         if profile:
645             osprofiler_profiler.init(options.profile)
646
647         client = self._get_versioned_client(api_version, args,
648                                             force_auth=False)
649
650         try:
651             args.func(client, args)
652         except exc.Unauthorized:
653             raise exc.CommandError("Invalid OpenStack Identity credentials.")
654         except Exception:
655             # NOTE(kragniz) Print any exceptions raised to stderr if the
656             # --debug flag is set
657             if args.debug:
658                 traceback.print_exc()
659             raise
660         finally:
661             if profile:
662                 trace_id = osprofiler_profiler.get().get_base_id()
663                 print("Profiling trace ID: %s" % trace_id)
664                 print("To display trace use next command:\n"
665                       "osprofiler trace show --html %s " % trace_id)
666
667     @utils.arg('command', metavar='<subcommand>', nargs='?',
668                help='Display help for <subcommand>.')
669     def do_help(self, args):
670         """
671         Display help about this program or one of its subcommands.
672         """
673         if getattr(args, 'command', None):
674             if args.command in self.subcommands:
675                 self.subcommands[args.command].print_help()
676             else:
677                 raise exc.CommandError("'%s' is not a valid subcommand" %
678                                        args.command)
679         else:
680             self.parser.print_help()
681
682     def do_bash_completion(self, _args):
683         """Prints arguments for bash_completion.
684
685         Prints all of the commands and options to stdout so that the
686         escalator.bash_completion script doesn't have to hard code them.
687         """
688         commands = set()
689         options = set()
690         for sc_str, sc in self.subcommands.items():
691             commands.add(sc_str)
692             for option in sc._optionals._option_string_actions.keys():
693                 options.add(option)
694
695         commands.remove('bash_completion')
696         commands.remove('bash-completion')
697         print(' '.join(commands | options))
698
699
700 class HelpFormatter(argparse.HelpFormatter):
701
702     def start_section(self, heading):
703         # Title-case the headings
704         heading = '%s%s' % (heading[0].upper(), heading[1:])
705         super(HelpFormatter, self).start_section(heading)
706
707
708 def main():
709     try:
710         escalatorShell().main(map(encodeutils.safe_decode, sys.argv[1:]))
711     except KeyboardInterrupt:
712         utils.exit('... terminating escalator client', exit_code=130)
713     except Exception as e:
714         utils.exit(utils.exception_to_str(e))