From ff36501bf1f9557b86743044172ae388bc751802 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Thu, 20 Nov 2014 20:02:08 -0800 Subject: [PATCH] Skeleton for the cli client Most of this code is based on the saharaclient Change-Id: I2af95da43273fa8636f490d62b6e71abc8e02eb0 --- magnumclient/api/__init__.py | 0 magnumclient/api/client.py | 83 +++++ magnumclient/api/httpclient.py | 42 +++ magnumclient/api/shell.py | 106 ++++++ magnumclient/shell.py | 662 +++++++++++++++++++++++++++++++++ magnumclient/version.py | 18 + requirements.txt | 1 + setup.cfg | 2 +- 8 files changed, 913 insertions(+), 1 deletion(-) create mode 100644 magnumclient/api/__init__.py create mode 100644 magnumclient/api/client.py create mode 100644 magnumclient/api/httpclient.py create mode 100644 magnumclient/api/shell.py create mode 100644 magnumclient/shell.py create mode 100644 magnumclient/version.py diff --git a/magnumclient/api/__init__.py b/magnumclient/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/magnumclient/api/client.py b/magnumclient/api/client.py new file mode 100644 index 00000000..74491abf --- /dev/null +++ b/magnumclient/api/client.py @@ -0,0 +1,83 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keystoneclient.v2_0 import client as keystone_client_v2 +from keystoneclient.v3 import client as keystone_client_v3 + +from magnumclient.api import httpclient + + +class Client(object): + def __init__(self, username=None, api_key=None, project_id=None, + project_name=None, auth_url=None, magnum_url=None, + endpoint_type='publicURL', service_type='data_processing', + input_auth_token=None): + + if not input_auth_token: + keystone = self.get_keystone_client(username=username, + api_key=api_key, + auth_url=auth_url, + project_id=project_id, + project_name=project_name) + input_auth_token = keystone.auth_token + if not input_auth_token: + raise RuntimeError("Not Authorized") + + magnum_catalog_url = magnum_url + if not magnum_url: + keystone = self.get_keystone_client(username=username, + api_key=api_key, + auth_url=auth_url, + token=input_auth_token, + project_id=project_id, + project_name=project_name) + catalog = keystone.service_catalog.get_endpoints(service_type) + if service_type in catalog: + for e_type, endpoint in catalog.get(service_type)[0].items(): + if str(e_type).lower() == str(endpoint_type).lower(): + magnum_catalog_url = endpoint + break + if not magnum_catalog_url: + raise RuntimeError("Could not find Magnum endpoint in catalog") + + self.client = httpclient.HTTPClient(magnum_catalog_url, + input_auth_token) + + def get_keystone_client(self, username=None, api_key=None, auth_url=None, + token=None, project_id=None, project_name=None): + if not auth_url: + raise RuntimeError("No auth url specified") + imported_client = (keystone_client_v2 if "v2.0" in auth_url + else keystone_client_v3) + if not getattr(self, "keystone_client", None): + self.keystone_client = imported_client.Client( + username=username, + password=api_key, + token=token, + tenant_id=project_id, + tenant_name=project_name, + auth_url=auth_url, + endpoint=auth_url) + + self.keystone_client.authenticate() + + return self.keystone_client + + @staticmethod + def get_projects_list(keystone_client): + if isinstance(keystone_client, keystone_client_v2.Client): + return keystone_client.tenants + + return keystone_client.projects diff --git a/magnumclient/api/httpclient.py b/magnumclient/api/httpclient.py new file mode 100644 index 00000000..e97ca569 --- /dev/null +++ b/magnumclient/api/httpclient.py @@ -0,0 +1,42 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + + +class HTTPClient(object): + def __init__(self, base_url, token): + self.base_url = base_url + self.token = token + + def get(self, url): + return requests.get(self.base_url + url, + headers={'x-auth-token': self.token}) + + def post(self, url, body, json=True): + headers = {'x-auth-token': self.token} + if json: + headers['content-type'] = 'application/json' + return requests.post(self.base_url + url, body, headers=headers) + + def put(self, url, body, json=True): + headers = {'x-auth-token': self.token} + if json: + headers['content-type'] = 'application/json' + return requests.put(self.base_url + url, body, headers=headers) + + def delete(self, url): + return requests.delete(self.base_url + url, + headers={'x-auth-token': self.token}) diff --git a/magnumclient/api/shell.py b/magnumclient/api/shell.py new file mode 100644 index 00000000..0f375c2e --- /dev/null +++ b/magnumclient/api/shell.py @@ -0,0 +1,106 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def do_bay_list(cs, args): + pass + + +def do_bay_create(cs, args): + pass + + +def do_bay_delete(cs, args): + pass + + +def do_bay_show(cs, args): + pass + + +def do_pod_list(cs, args): + pass + + +def do_pod_create(cs, args): + pass + + +def do_pod_delete(cs, args): + pass + + +def do_pod_show(cs, args): + pass + + +def do_service_list(cs, args): + pass + + +def do_service_create(cs, args): + pass + + +def do_service_delete(cs, args): + pass + + +def do_service_show(cs, args): + pass + + +def do_container_create(cs, args): + pass + + +def do_container_list(cs, args): + pass + + +def do_container_delete(cs, args): + pass + + +def do_container_show(cs, args): + pass + + +def do_container_reboot(cs, args): + pass + + +def do_container_stop(cs, args): + pass + + +def do_container_start(cs, args): + pass + + +def do_container_pause(cs, args): + pass + + +def do_container_unpause(cs, args): + pass + + +def do_container_logs(cs, args): + pass + + +def do_container_execute(cs, args): + pass diff --git a/magnumclient/shell.py b/magnumclient/shell.py new file mode 100644 index 00000000..20c81483 --- /dev/null +++ b/magnumclient/shell.py @@ -0,0 +1,662 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +### +# This code is taken from python-novaclient. Goal is minimal modification. +### + +""" +Command-line interface to the OpenStack Magnum API. +""" + +from __future__ import print_function +import argparse +import getpass +import logging +import sys + +from oslo.utils import encodeutils +from oslo.utils import strutils +import six + +HAS_KEYRING = False +all_errors = ValueError +try: + import keyring + HAS_KEYRING = True + try: + if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): + import gnomekeyring + all_errors = (ValueError, + gnomekeyring.IOError, + gnomekeyring.NoKeyringDaemonError) + except Exception: + pass +except ImportError: + pass + +from magnumclient.api import client +from magnumclient.api import shell as shell_api +from magnumclient.openstack.common.apiclient import auth +from magnumclient.openstack.common.apiclient import exceptions as exc +from magnumclient.openstack.common import cliutils +from magnumclient import version + +DEFAULT_API_VERSION = 'api' +DEFAULT_ENDPOINT_TYPE = 'publicURL' +DEFAULT_SERVICE_TYPE = 'data_processing' + +logger = logging.getLogger(__name__) + + +def positive_non_zero_float(text): + if text is None: + return None + try: + value = float(text) + except ValueError: + msg = "%s must be a float" % text + raise argparse.ArgumentTypeError(msg) + if value <= 0: + msg = "%s must be greater than 0" % text + raise argparse.ArgumentTypeError(msg) + return value + + +class SecretsHelper(object): + def __init__(self, args, client): + self.args = args + self.client = client + self.key = None + + def _validate_string(self, text): + if text is None or len(text) == 0: + return False + return True + + def _make_key(self): + if self.key is not None: + return self.key + keys = [ + self.client.auth_url, + self.client.projectid, + self.client.user, + self.client.region_name, + self.client.endpoint_type, + self.client.service_type, + self.client.service_name, + self.client.volume_service_name, + ] + for (index, key) in enumerate(keys): + if key is None: + keys[index] = '?' + else: + keys[index] = str(keys[index]) + self.key = "/".join(keys) + return self.key + + def _prompt_password(self, verify=True): + pw = None + if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): + # Check for Ctl-D + try: + while True: + pw1 = getpass.getpass('OS Password: ') + if verify: + pw2 = getpass.getpass('Please verify: ') + else: + pw2 = pw1 + if pw1 == pw2 and self._validate_string(pw1): + pw = pw1 + break + except EOFError: + pass + return pw + + def save(self, auth_token, management_url, tenant_id): + if not HAS_KEYRING or not self.args.os_cache: + return + if (auth_token == self.auth_token and + management_url == self.management_url): + # Nothing changed.... + return + if not all([management_url, auth_token, tenant_id]): + raise ValueError("Unable to save empty management url/auth token") + value = "|".join([str(auth_token), + str(management_url), + str(tenant_id)]) + keyring.set_password("magnumclient_auth", self._make_key(), value) + + @property + def password(self): + if self._validate_string(self.args.os_password): + return self.args.os_password + verify_pass = ( + strutils.bool_from_string(cliutils.env("OS_VERIFY_PASSWORD")) + ) + return self._prompt_password(verify_pass) + + @property + def management_url(self): + if not HAS_KEYRING or not self.args.os_cache: + return None + management_url = None + try: + block = keyring.get_password('magnumclient_auth', + self._make_key()) + if block: + _token, management_url, _tenant_id = block.split('|', 2) + except all_errors: + pass + return management_url + + @property + def auth_token(self): + # Now is where it gets complicated since we + # want to look into the keyring module, if it + # exists and see if anything was provided in that + # file that we can use. + if not HAS_KEYRING or not self.args.os_cache: + return None + token = None + try: + block = keyring.get_password('magnumclient_auth', + self._make_key()) + if block: + token, _management_url, _tenant_id = block.split('|', 2) + except all_errors: + pass + return token + + @property + def tenant_id(self): + if not HAS_KEYRING or not self.args.os_cache: + return None + tenant_id = None + try: + block = keyring.get_password('magnumclient_auth', + self._make_key()) + if block: + _token, _management_url, tenant_id = block.split('|', 2) + except all_errors: + pass + return tenant_id + + +class MagnumClientArgumentParser(argparse.ArgumentParser): + + def __init__(self, *args, **kwargs): + super(MagnumClientArgumentParser, self).__init__(*args, **kwargs) + + def error(self, message): + """error(message: string) + + Prints a usage message incorporating the message to stderr and + exits. + """ + self.print_usage(sys.stderr) + # FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value + choose_from = ' (choose from' + progparts = self.prog.partition(' ') + self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" + " for more information.\n" % + {'errmsg': message.split(choose_from)[0], + 'mainp': progparts[0], + 'subp': progparts[2]}) + + +class OpenStackMagnumShell(object): + + def get_base_parser(self): + parser = MagnumClientArgumentParser( + prog='magnum', + description=__doc__.strip(), + epilog='See "magnum help COMMAND" ' + 'for help on a specific command.', + add_help=False, + formatter_class=OpenStackHelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', '--help', + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--version', + action='version', + version=version.version_info.version_string()) + + parser.add_argument('--debug', + default=False, + action='store_true', + help="Print debugging output.") + + parser.add_argument('--os-cache', + default=strutils.bool_from_string( + cliutils.env('OS_CACHE', default=False)), + action='store_true', + help="Use the auth token cache. Defaults to False " + "if env[OS_CACHE] is not set.") + +# TODO(mattf) - add get_timings support to Client +# parser.add_argument('--timings', +# default=False, +# action='store_true', +# help="Print call timing info") + +# TODO(mattf) - use timeout +# parser.add_argument('--timeout', +# default=600, +# metavar='', +# type=positive_non_zero_float, +# help="Set HTTP call timeout (in seconds)") + + parser.add_argument('--os-tenant-id', + metavar='', + default=cliutils.env('OS_TENANT_ID'), + help='Defaults to env[OS_TENANT_ID].') + +# NA +# parser.add_argument('--os-region-name', +# metavar='', +# default=cliutils.env('OS_REGION_NAME', 'SAHARA_REGION_NAME'), +# help='Defaults to env[OS_REGION_NAME].') +# parser.add_argument('--os_region_name', +# help=argparse.SUPPRESS) + + parser.add_argument('--service-type', + metavar='', + help='Defaults to data_processing for all ' + 'actions.') + parser.add_argument('--service_type', + help=argparse.SUPPRESS) + +# NA +# parser.add_argument('--service-name', +# metavar='', +# default=utils.env('SAHARA_SERVICE_NAME'), +# help='Defaults to env[SAHARA_SERVICE_NAME]') +# parser.add_argument('--service_name', +# help=argparse.SUPPRESS) + +# NA +# parser.add_argument('--volume-service-name', +# metavar='', +# default=utils.env('NOVA_VOLUME_SERVICE_NAME'), +# help='Defaults to env[NOVA_VOLUME_SERVICE_NAME]') +# parser.add_argument('--volume_service_name', +# help=argparse.SUPPRESS) + + parser.add_argument('--endpoint-type', + metavar='', + default=cliutils.env( + 'MAGNUM_ENDPOINT_TYPE', + default=DEFAULT_ENDPOINT_TYPE), + help='Defaults to env[MAGNUM_ENDPOINT_TYPE] or ' + + DEFAULT_ENDPOINT_TYPE + '.') + # NOTE(dtroyer): We can't add --endpoint_type here due to argparse + # thinking usage-list --end is ambiguous; but it + # works fine with only --endpoint-type present + # Go figure. I'm leaving this here for doc purposes. + # parser.add_argument('--endpoint_type', + # help=argparse.SUPPRESS) + + parser.add_argument('--magnum-api-version', + metavar='', + default=cliutils.env( + 'MAGNUM_API_VERSION', + default=DEFAULT_API_VERSION), + help='Accepts "api", ' + 'defaults to env[MAGNUM_API_VERSION].') + parser.add_argument('--magnum_api_version', + help=argparse.SUPPRESS) + + parser.add_argument('--os-cacert', + metavar='', + default=cliutils.env('OS_CACERT', default=None), + help='Specify a CA bundle file to use in ' + 'verifying a TLS (https) server certificate. ' + 'Defaults to env[OS_CACERT].') + +# NA +# parser.add_argument('--insecure', +# default=utils.env('NOVACLIENT_INSECURE', default=False), +# action='store_true', +# help="Explicitly allow novaclient to perform \"insecure\" " +# "SSL (https) requests. The server's certificate will " +# "not be verified against any certificate authorities. " +# "This option should be used with caution.") + + parser.add_argument('--bypass-url', + metavar='', + default=cliutils.env('BYPASS_URL', default=None), + dest='bypass_url', + help="Use this API endpoint instead of the " + "Service Catalog.") + parser.add_argument('--bypass_url', + help=argparse.SUPPRESS) + + # The auth-system-plugins might require some extra options + auth.load_auth_system_opts(parser) + + return parser + + def get_subcommand_parser(self, version): + parser = self.get_base_parser() + + self.subcommands = {} + subparsers = parser.add_subparsers(metavar='') + + try: + actions_module = { + 'api': shell_api, + }[version] + except KeyError: + actions_module = shell_api + actions_module = shell_api + + self._find_actions(subparsers, actions_module) + self._find_actions(subparsers, self) + + self._add_bash_completion_subparser(subparsers) + + return parser + + def _add_bash_completion_subparser(self, subparsers): + subparser = ( + subparsers.add_parser('bash_completion', + add_help=False, + formatter_class=OpenStackHelpFormatter) + ) + self.subcommands['bash_completion'] = subparser + subparser.set_defaults(func=self.do_bash_completion) + + def _find_actions(self, subparsers, actions_module): + for attr in (a for a in dir(actions_module) if a.startswith('do_')): + # I prefer to be hyphen-separated instead of underscores. + command = attr[3:].replace('_', '-') + callback = getattr(actions_module, attr) + desc = callback.__doc__ or '' + action_help = desc.strip() + arguments = getattr(callback, 'arguments', []) + + subparser = ( + subparsers.add_parser(command, + help=action_help, + description=desc, + add_help=False, + formatter_class=OpenStackHelpFormatter) + ) + subparser.add_argument('-h', '--help', + action='help', + help=argparse.SUPPRESS,) + self.subcommands[command] = subparser + for (args, kwargs) in arguments: + subparser.add_argument(*args, **kwargs) + subparser.set_defaults(func=callback) + + def setup_debugging(self, debug): + if not debug: + return + + streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" + # Set up the root logger to debug so that the submodules can + # print debug messages + logging.basicConfig(level=logging.DEBUG, + format=streamformat) + + def main(self, argv): + + # Parse args once to find version and debug settings + parser = self.get_base_parser() + (options, args) = parser.parse_known_args(argv) + self.setup_debugging(options.debug) + + # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse + # thinking usage-list --end is ambiguous; but it + # works fine with only --endpoint-type present + # Go figure. + if '--endpoint_type' in argv: + spot = argv.index('--endpoint_type') + argv[spot] = '--endpoint-type' + + subcommand_parser = ( + self.get_subcommand_parser(options.magnum_api_version) + ) + self.parser = subcommand_parser + + if options.help or not argv: + subcommand_parser.print_help() + return 0 + + args = subcommand_parser.parse_args(argv) + + # Short-circuit and deal with help right away. + if args.func == self.do_help: + self.do_help(args) + return 0 + elif args.func == self.do_bash_completion: + self.do_bash_completion(args) + return 0 + +# (os_username, os_tenant_name, os_tenant_id, os_auth_url, +# os_region_name, os_auth_system, endpoint_type, insecure, +# service_type, service_name, volume_service_name, +# bypass_url, os_cache, cacert) = ( #, timeout) = ( +# args.os_username, +# args.os_tenant_name, args.os_tenant_id, +# args.os_auth_url, +# args.os_region_name, +# args.os_auth_system, +# args.endpoint_type, args.insecure, +# args.service_type, +# args.service_name, args.volume_service_name, +# args.bypass_url, args.os_cache, +# args.os_cacert, args.timeout) + (os_username, os_tenant_name, os_tenant_id, + os_auth_url, os_auth_system, endpoint_type, + service_type, bypass_url) = ( + (args.os_username, args.os_tenant_name, args.os_tenant_id, + args.os_auth_url, args.os_auth_system, args.endpoint_type, + args.service_type, args.bypass_url) + ) + + if os_auth_system and os_auth_system != "keystone": + auth_plugin = auth.load_plugin(os_auth_system) + else: + auth_plugin = None + + # Fetched and set later as needed + os_password = None + + if not endpoint_type: + endpoint_type = DEFAULT_ENDPOINT_TYPE + + if not service_type: + service_type = DEFAULT_SERVICE_TYPE +# NA - there is only one service this CLI accesses +# service_type = utils.get_service_type(args.func) or service_type + + # FIXME(usrleon): Here should be restrict for project id same as + # for os_username or os_password but for compatibility it is not. + if not cliutils.isunauthenticated(args.func): + if auth_plugin: + auth_plugin.parse_opts(args) + + if not auth_plugin or not auth_plugin.opts: + if not os_username: + raise exc.CommandError("You must provide a username " + "via either --os-username or " + "env[OS_USERNAME]") + + if not os_tenant_name and not os_tenant_id: + raise exc.CommandError("You must provide a tenant name " + "or tenant id via --os-tenant-name, " + "--os-tenant-id, env[OS_TENANT_NAME] " + "or env[OS_TENANT_ID]") + + if not os_auth_url: + if os_auth_system and os_auth_system != 'keystone': + os_auth_url = auth_plugin.get_auth_url() + + if not os_auth_url: + raise exc.CommandError("You must provide an auth url " + "via either --os-auth-url or " + "env[OS_AUTH_URL] or specify an " + "auth_system which defines a " + "default url with --os-auth-system " + "or env[OS_AUTH_SYSTEM]") + +# NA +# if (options.os_compute_api_version and +# options.os_compute_api_version != '1.0'): +# if not os_tenant_name and not os_tenant_id: +# raise exc.CommandError("You must provide a tenant name " +# "or tenant id via --os-tenant-name, " +# "--os-tenant-id, env[OS_TENANT_NAME] " +# "or env[OS_TENANT_ID]") +# +# if not os_auth_url: +# raise exc.CommandError("You must provide an auth url " +# "via either --os-auth-url or env[OS_AUTH_URL]") + +# NOTE: The Sahara client authenticates when you create it. So instead of +# creating here and authenticating later, which is what the novaclient +# does, we just create the client later. + + # Now check for the password/token of which pieces of the + # identifying keyring key can come from the underlying client + if not cliutils.isunauthenticated(args.func): + # NA - Client can't be used with SecretsHelper + # helper = SecretsHelper(args, self.cs.client) + if (auth_plugin and auth_plugin.opts and + "os_password" not in auth_plugin.opts): + use_pw = False + else: + use_pw = True + +# tenant_id, auth_token, management_url = (helper.tenant_id, +# helper.auth_token, +# helper.management_url) +# +# if tenant_id and auth_token and management_url: +# self.cs.client.tenant_id = tenant_id +# self.cs.client.auth_token = auth_token +# self.cs.client.management_url = management_url +# # authenticate just sets up some values in this case, no REST +# # calls +# self.cs.authenticate() + if use_pw: + # Auth using token must have failed or not happened + # at all, so now switch to password mode and save + # the token when its gotten... using our keyring + # saver + # os_password = helper.password + os_password = args.os_password + if not os_password: + raise exc.CommandError( + 'Expecting a password provided via either ' + '--os-password, env[OS_PASSWORD], or ' + 'prompted response') +# self.cs.client.password = os_password +# self.cs.client.keyring_saver = helper + +# NA +# try: +# if not utils.isunauthenticated(args.func): +# self.cs.authenticate() +# except exc.Unauthorized: +# raise exc.CommandError("Invalid OpenStack Sahara credentials.") +# except exc.AuthorizationFailure: +# raise exc.CommandError("Unable to authorize user") + + self.cs = client.Client(username=os_username, + api_key=os_password, + project_id=os_tenant_id, + project_name=os_tenant_name, + auth_url=os_auth_url, + magnum_url=bypass_url) + + args.func(self.cs, args) + +# TODO(mattf) - add get_timings support to Client +# if args.timings: +# self._dump_timings(self.cs.get_timings()) + + def _dump_timings(self, timings): + class Tyme(object): + def __init__(self, url, seconds): + self.url = url + self.seconds = seconds + results = [Tyme(url, end - start) for url, start, end in timings] + total = 0.0 + for tyme in results: + total += tyme.seconds + results.append(Tyme("Total", total)) + cliutils.print_list(results, ["url", "seconds"], sortby_index=None) + + def do_bash_completion(self, _args): + """Prints arguments for bash-completion. + + Prints all of the commands and options to stdout so that the + magnum.bash_completion script doesn't have to hard code them. + """ + commands = set() + options = set() + for sc_str, sc in self.subcommands.items(): + commands.add(sc_str) + for option in sc._optionals._option_string_actions.keys(): + options.add(option) + + commands.remove('bash-completion') + commands.remove('bash_completion') + print(' '.join(commands | options)) + + @cliutils.arg('command', metavar='', nargs='?', + help='Display help for .') + def do_help(self, args): + """Display help about this program or one of its subcommands.""" + if args.command: + if args.command in self.subcommands: + self.subcommands[args.command].print_help() + else: + raise exc.CommandError("'%s' is not a valid subcommand" % + args.command) + else: + self.parser.print_help() + + +# I'm picky about my shell help. +class OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + # Title-case the headings + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +def main(): + try: + OpenStackMagnumShell().main(map(encodeutils.safe_decode, sys.argv[1:])) + + except Exception as e: + logger.debug(e, exc_info=1) + print("ERROR: %s" % encodeutils.safe_encode(six.text_type(e)), + file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/magnumclient/version.py b/magnumclient/version.py new file mode 100644 index 00000000..fc55c14f --- /dev/null +++ b/magnumclient/version.py @@ -0,0 +1,18 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pbr import version + +version_info = version.VersionInfo('python-magnumclient') diff --git a/requirements.txt b/requirements.txt index fe26924c..7793ebbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ pbr>=0.6,!=0.7,<1.0 Babel>=1.3 oslo.config>=1.4.0 # Apache-2.0 +oslo.utils>=1.0.0 # Apache-2.0 iso8601>=0.1.9 requests>=2.2.0,!=2.4.0 python-keystoneclient>=0.11.1 diff --git a/setup.cfg b/setup.cfg index 666333d8..715e17df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ packages = [entry_points] console_scripts = - magnum = magnumclient.magnum:main + magnum = magnumclient.shell:main [build_sphinx] source-dir = doc/source