1#    Licensed under the Apache License, Version 2.0 (the "License"); you may
2#    not use this file except in compliance with the License. You may obtain
3#    a copy of the License at
4#
5#         http://www.apache.org/licenses/LICENSE-2.0
6#
7#    Unless required by applicable law or agreed to in writing, software
8#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10#    License for the specific language governing permissions and limitations
11#    under the License.
12
13"""
14Command-line interface to the OpenStack Telemetry API.
15"""
16
17from __future__ import print_function
18
19import argparse
20import logging
21import sys
22import warnings
23
24from oslo_utils import encodeutils
25from oslo_utils import importutils
26import six
27
28import ceilometerclient
29from ceilometerclient import client as ceiloclient
30from ceilometerclient.common import utils
31from ceilometerclient import exc
32
33
34def _positive_non_zero_int(argument_value):
35    if argument_value is None:
36        return None
37    try:
38        value = int(argument_value)
39    except ValueError:
40        msg = "%s must be an integer" % argument_value
41        raise argparse.ArgumentTypeError(msg)
42    if value <= 0:
43        msg = "%s must be greater than 0" % argument_value
44        raise argparse.ArgumentTypeError(msg)
45    return value
46
47
48class CeilometerShell(object):
49
50    def __init__(self):
51        self.auth_plugin = ceiloclient.AuthPlugin()
52
53    def get_base_parser(self):
54        parser = argparse.ArgumentParser(
55            prog='ceilometer',
56            description=__doc__.strip(),
57            epilog='See "ceilometer help COMMAND" '
58                   'for help on a specific command.',
59            add_help=False,
60            formatter_class=HelpFormatter,
61        )
62
63        # Global arguments
64        parser.add_argument('-h', '--help',
65                            action='store_true',
66                            help=argparse.SUPPRESS,
67                            )
68
69        parser.add_argument('--version',
70                            action='version',
71                            version=ceilometerclient.__version__)
72
73        parser.add_argument('-d', '--debug',
74                            default=bool(utils.env('CEILOMETERCLIENT_DEBUG')
75                                         ),
76                            action='store_true',
77                            help='Defaults to env[CEILOMETERCLIENT_DEBUG].')
78
79        parser.add_argument('-v', '--verbose',
80                            default=False, action="store_true",
81                            help="Print more verbose output.")
82
83        parser.add_argument('--timeout',
84                            default=600,
85                            type=_positive_non_zero_int,
86                            help='Number of seconds to wait for a response.')
87
88        parser.add_argument('--ceilometer-url', metavar='<CEILOMETER_URL>',
89                            dest='os_endpoint',
90                            default=utils.env('CEILOMETER_URL'),
91                            help=("DEPRECATED, use --os-endpoint instead. "
92                                  "Defaults to env[CEILOMETER_URL]."))
93
94        parser.add_argument('--ceilometer_url',
95                            dest='os_endpoint',
96                            help=argparse.SUPPRESS)
97
98        parser.add_argument('--ceilometer-api-version',
99                            default=utils.env(
100                                'CEILOMETER_API_VERSION', default='2'),
101                            help='Defaults to env[CEILOMETER_API_VERSION] '
102                            'or 2.')
103
104        parser.add_argument('--ceilometer_api_version',
105                            help=argparse.SUPPRESS)
106
107        self.auth_plugin.add_opts(parser)
108        self.auth_plugin.add_common_opts(parser)
109
110        return parser
111
112    def get_subcommand_parser(self, version):
113        parser = self.get_base_parser()
114
115        self.subcommands = {}
116        subparsers = parser.add_subparsers(metavar='<subcommand>')
117        submodule = importutils.import_versioned_module('ceilometerclient',
118                                                        version, 'shell')
119        self._find_actions(subparsers, submodule)
120        self._find_actions(subparsers, self)
121
122        return parser
123
124    def _find_actions(self, subparsers, actions_module):
125        for attr in (a for a in dir(actions_module) if a.startswith('do_')):
126            # I prefer to be hypen-separated instead of underscores.
127            command = attr[3:].replace('_', '-')
128            callback = getattr(actions_module, attr)
129            desc = callback.__doc__ or ''
130            help = desc.strip().split('\n')[0]
131            arguments = getattr(callback, 'arguments', [])
132
133            subparser = subparsers.add_parser(command, help=help,
134                                              description=desc,
135                                              add_help=False,
136                                              formatter_class=HelpFormatter)
137            subparser.add_argument('-h', '--help', action='help',
138                                   help=argparse.SUPPRESS)
139            self.subcommands[command] = subparser
140            for (args, kwargs) in arguments:
141                subparser.add_argument(*args, **kwargs)
142            subparser.set_defaults(func=callback)
143
144    @staticmethod
145    def _setup_logging(debug):
146        format = '%(levelname)s (%(module)s) %(message)s'
147        if debug:
148            logging.basicConfig(format=format, level=logging.DEBUG)
149        else:
150            logging.basicConfig(format=format, level=logging.WARN)
151        logging.getLogger('iso8601').setLevel(logging.WARNING)
152        logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
153
154    def parse_args(self, argv):
155        # Parse args once to find version
156        parser = self.get_base_parser()
157        (options, args) = parser.parse_known_args(argv)
158        self.auth_plugin.parse_opts(options)
159        self._setup_logging(options.debug)
160
161        # build available subcommands based on version
162        api_version = options.ceilometer_api_version
163        subcommand_parser = self.get_subcommand_parser(api_version)
164        self.parser = subcommand_parser
165
166        # Handle top-level --help/-h before attempting to parse
167        # a command off the command line
168        if options.help or not argv:
169            self.do_help(options)
170            return 0
171
172        # Return parsed args
173        return api_version, subcommand_parser.parse_args(argv)
174
175    def main(self, argv):
176        warnings.warn(
177            "ceilometerclient is now deprecated as the Ceilometer API has "
178            "been deprecated. Please use either aodhclient, pankoclient or "
179            "gnocchiclient.")
180        parsed = self.parse_args(argv)
181        if parsed == 0:
182            return 0
183        api_version, args = parsed
184
185        # Short-circuit and deal with help command right away.
186        if args.func == self.do_help:
187            self.do_help(args)
188            return 0
189        elif args.func == self.do_bash_completion:
190            self.do_bash_completion(args)
191            return 0
192
193        if not ((self.auth_plugin.opts.get('token')
194                 or self.auth_plugin.opts.get('auth_token'))
195                and self.auth_plugin.opts['endpoint']):
196            if not self.auth_plugin.opts['username']:
197                raise exc.CommandError("You must provide a username via "
198                                       "either --os-username or via "
199                                       "env[OS_USERNAME]")
200
201            if not self.auth_plugin.opts['password']:
202                raise exc.CommandError("You must provide a password via "
203                                       "either --os-password or via "
204                                       "env[OS_PASSWORD]")
205
206            if not (args.os_project_id or args.os_project_name
207                    or args.os_tenant_id or args.os_tenant_name):
208                # steer users towards Keystone V3 API
209                raise exc.CommandError("You must provide a project_id "
210                                       "(or name) via either --os-project-id "
211                                       "or via env[OS_PROJECT_ID]")
212
213            if not self.auth_plugin.opts['auth_url']:
214                raise exc.CommandError("You must provide an auth url via "
215                                       "either --os-auth-url or via "
216                                       "env[OS_AUTH_URL]")
217
218        client_kwargs = vars(args)
219        client_kwargs.update(self.auth_plugin.opts)
220        client_kwargs['auth_plugin'] = self.auth_plugin
221        client = ceiloclient.get_client(api_version, **client_kwargs)
222        # call whatever callback was selected
223        try:
224            args.func(client, args)
225        except exc.HTTPUnauthorized:
226            raise exc.CommandError("Invalid OpenStack Identity credentials.")
227
228    def do_bash_completion(self, args):
229        """Prints all of the commands and options to stdout.
230
231        The ceilometer.bash_completion script doesn't have to hard code them.
232        """
233        commands = set()
234        options = set()
235        for sc_str, sc in self.subcommands.items():
236            commands.add(sc_str)
237            for option in list(sc._optionals._option_string_actions):
238                options.add(option)
239
240        commands.remove('bash-completion')
241        print(' '.join(commands | options))
242
243    @utils.arg('command', metavar='<subcommand>', nargs='?',
244               help='Display help for <subcommand>')
245    def do_help(self, args):
246        """Display help about this program or one of its subcommands."""
247        if getattr(args, 'command', None):
248            if args.command in self.subcommands:
249                self.subcommands[args.command].print_help()
250            else:
251                raise exc.CommandError("'%s' is not a valid subcommand" %
252                                       args.command)
253        else:
254            self.parser.print_help()
255
256
257class HelpFormatter(argparse.HelpFormatter):
258    def __init__(self, prog, indent_increment=2, max_help_position=32,
259                 width=None):
260        super(HelpFormatter, self).__init__(prog, indent_increment,
261                                            max_help_position, width)
262
263    def start_section(self, heading):
264        # Title-case the headings
265        heading = '%s%s' % (heading[0].upper(), heading[1:])
266        super(HelpFormatter, self).start_section(heading)
267
268
269def main(args=None):
270    try:
271        if args is None:
272            args = sys.argv[1:]
273
274        CeilometerShell().main(args)
275
276    except Exception as e:
277        if '--debug' in args or '-d' in args:
278            raise
279        else:
280            print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
281        sys.exit(1)
282    except KeyboardInterrupt:
283        print("Stopping Ceilometer Client", file=sys.stderr)
284        sys.exit(130)
285
286if __name__ == "__main__":
287    main()
288