diff options
Diffstat (limited to 'openstackclient')
21 files changed, 699 insertions, 215 deletions
diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 14bb01d7..ba51bee1 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -16,13 +16,9 @@ import argparse import logging -from six.moves.urllib import parse as urlparse import stevedore -from oslo.config import cfg - from keystoneclient.auth import base -from keystoneclient.auth.identity.generic import password as ksc_password from openstackclient.common import exceptions as exc from openstackclient.common import utils @@ -85,9 +81,9 @@ def select_auth_plugin(options): # let keystoneclient figure it out itself auth_plugin_name = 'token' else: - raise exc.CommandError( - "Authentication type must be selected with --os-auth-type" - ) + # The ultimate default is similar to the original behaviour, + # but this time with version discovery + auth_plugin_name = 'osc_password' LOG.debug("Auth plugin %s selected" % auth_plugin_name) return auth_plugin_name @@ -201,96 +197,3 @@ def build_auth_plugins_option_parser(parser): help=argparse.SUPPRESS, ) return parser - - -class TokenEndpoint(base.BaseAuthPlugin): - """Auth plugin to handle traditional token/endpoint usage - - Implements the methods required to handle token authentication - with a user-specified token and service endpoint; no Identity calls - are made for re-scoping, service catalog lookups or the like. - - The purpose of this plugin is to get rid of the special-case paths - in the code to handle this authentication format. Its primary use - is for bootstrapping the Keystone database. - """ - - def __init__(self, url, token, **kwargs): - """A plugin for static authentication with an existing token - - :param string url: Service endpoint - :param string token: Existing token - """ - super(TokenEndpoint, self).__init__() - self.endpoint = url - self.token = token - - def get_endpoint(self, session, **kwargs): - """Return the supplied endpoint""" - return self.endpoint - - def get_token(self, session): - """Return the supplied token""" - return self.token - - def get_auth_ref(self, session, **kwargs): - """Stub this method for compatibility""" - return None - - # Override this because it needs to be a class method... - @classmethod - def get_options(self): - options = super(TokenEndpoint, self).get_options() - - options.extend([ - # Maintain name 'url' for compatibility - cfg.StrOpt('url', - help='Specific service endpoint to use'), - cfg.StrOpt('token', - secret=True, - help='Authentication token to use'), - ]) - - return options - - -class OSCGenericPassword(ksc_password.Password): - """Auth plugin hack to work around broken Keystone configurations - - The default Keystone configuration uses http://localhost:xxxx in - admin_endpoint and public_endpoint and are returned in the links.href - attribute by the version routes. Deployments that do not set these - are unusable with newer keystoneclient version discovery. - - """ - - def create_plugin(self, session, version, url, raw_status=None): - """Handle default Keystone endpoint configuration - - Build the actual API endpoint from the scheme, host and port of the - original auth URL and the rest from the returned version URL. - """ - - ver_u = urlparse.urlparse(url) - - # Only hack this if it is the default setting - if ver_u.netloc.startswith('localhost'): - auth_u = urlparse.urlparse(self.auth_url) - # from original auth_url: scheme, netloc - # from api_url: path, query (basically, the rest) - url = urlparse.urlunparse(( - auth_u.scheme, - auth_u.netloc, - ver_u.path, - ver_u.params, - ver_u.query, - ver_u.fragment, - )) - LOG.debug('Version URL updated: %s' % url) - - return super(OSCGenericPassword, self).create_plugin( - session=session, - version=version, - url=url, - raw_status=raw_status, - ) diff --git a/openstackclient/api/auth_plugin.py b/openstackclient/api/auth_plugin.py new file mode 100644 index 00000000..deddfcc4 --- /dev/null +++ b/openstackclient/api/auth_plugin.py @@ -0,0 +1,107 @@ +# 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. +# + +"""Authentication Plugin Library""" + +import logging + +from oslo_config import cfg +from six.moves.urllib import parse as urlparse + +from keystoneclient.auth.identity.generic import password as ksc_password +from keystoneclient.auth import token_endpoint + +LOG = logging.getLogger(__name__) + + +class TokenEndpoint(token_endpoint.Token): + """Auth plugin to handle traditional token/endpoint usage + + Implements the methods required to handle token authentication + with a user-specified token and service endpoint; no Identity calls + are made for re-scoping, service catalog lookups or the like. + + The purpose of this plugin is to get rid of the special-case paths + in the code to handle this authentication format. Its primary use + is for bootstrapping the Keystone database. + """ + + def __init__(self, url, token, **kwargs): + """A plugin for static authentication with an existing token + + :param string url: Service endpoint + :param string token: Existing token + """ + super(TokenEndpoint, self).__init__(endpoint=url, + token=token) + + def get_auth_ref(self, session, **kwargs): + # Stub this method for compatibility + return None + + @classmethod + def get_options(self): + options = super(TokenEndpoint, self).get_options() + + options.extend([ + # Maintain name 'url' for compatibility + cfg.StrOpt('url', + help='Specific service endpoint to use'), + cfg.StrOpt('token', + secret=True, + help='Authentication token to use'), + ]) + + return options + + +class OSCGenericPassword(ksc_password.Password): + """Auth plugin hack to work around broken Keystone configurations + + The default Keystone configuration uses http://localhost:xxxx in + admin_endpoint and public_endpoint and are returned in the links.href + attribute by the version routes. Deployments that do not set these + are unusable with newer keystoneclient version discovery. + + """ + + def create_plugin(self, session, version, url, raw_status=None): + """Handle default Keystone endpoint configuration + + Build the actual API endpoint from the scheme, host and port of the + original auth URL and the rest from the returned version URL. + """ + + ver_u = urlparse.urlparse(url) + + # Only hack this if it is the default setting + if ver_u.netloc.startswith('localhost'): + auth_u = urlparse.urlparse(self.auth_url) + # from original auth_url: scheme, netloc + # from api_url: path, query (basically, the rest) + url = urlparse.urlunparse(( + auth_u.scheme, + auth_u.netloc, + ver_u.path, + ver_u.params, + ver_u.query, + ver_u.fragment, + )) + LOG.debug('Version URL updated: %s' % url) + + return super(OSCGenericPassword, self).create_plugin( + session=session, + version=version, + url=url, + raw_status=raw_status, + ) diff --git a/openstackclient/api/image_v1.py b/openstackclient/api/image_v1.py index c363ce49..534c7750 100644 --- a/openstackclient/api/image_v1.py +++ b/openstackclient/api/image_v1.py @@ -19,11 +19,18 @@ from openstackclient.api import api class APIv1(api.BaseAPI): """Image v1 API""" + _endpoint_suffix = 'v1' + def __init__(self, endpoint=None, **kwargs): super(APIv1, self).__init__(endpoint=endpoint, **kwargs) + self.endpoint = self.endpoint.rstrip('/') + self._munge_url() + + def _munge_url(self): # Hack this until discovery is up - self.endpoint = '/'.join([self.endpoint.rstrip('/'), 'v1']) + if self._endpoint_suffix not in self.endpoint.split('/')[-1]: + self.endpoint = '/'.join([self.endpoint, self._endpoint_suffix]) def image_list( self, diff --git a/openstackclient/api/image_v2.py b/openstackclient/api/image_v2.py index 37c2ed83..d8bb2801 100644 --- a/openstackclient/api/image_v2.py +++ b/openstackclient/api/image_v2.py @@ -19,11 +19,12 @@ from openstackclient.api import image_v1 class APIv2(image_v1.APIv1): """Image v2 API""" - def __init__(self, endpoint=None, **kwargs): - super(APIv2, self).__init__(endpoint=endpoint, **kwargs) + _endpoint_suffix = 'v2' + def _munge_url(self): # Hack this until discovery is up, and ignore parent endpoint setting - self.endpoint = '/'.join([endpoint.rstrip('/'), 'v2']) + if 'v2' not in self.endpoint.split('/')[-1]: + self.endpoint = '/'.join([self.endpoint, 'v2']) def image_list( self, diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 974936f8..10f38c25 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -54,16 +54,18 @@ class ClientManager(object): for o in auth.OPTIONS_LIST]: return self._auth_params[name[1:]] + raise AttributeError(name) + def __init__( self, - auth_options, + cli_options, api_version=None, verify=True, pw_func=None, ): """Set up a ClientManager - :param auth_options: + :param cli_options: Options collected from the command-line, environment, or wherever :param api_version: Dict of API versions: key is API name, value is the version @@ -77,31 +79,57 @@ class ClientManager(object): returns a string containing the password """ + self._cli_options = cli_options + self._api_version = api_version + self._pw_callback = pw_func + self._url = self._cli_options.os_url + self._region_name = self._cli_options.os_region_name + + self.timing = self._cli_options.timing + + self._auth_ref = None + self.session = None + + # verify is the Requests-compatible form + self._verify = verify + # also store in the form used by the legacy client libs + self._cacert = None + if isinstance(verify, bool): + self._insecure = not verify + else: + self._cacert = verify + self._insecure = False + + # Get logging from root logger + root_logger = logging.getLogger('') + LOG.setLevel(root_logger.getEffectiveLevel()) + + def setup_auth(self): + """Set up authentication + + This is deferred until authentication is actually attempted because + it gets in the way of things that do not require auth. + """ + # If no auth type is named by the user, select one based on # the supplied options - self.auth_plugin_name = auth.select_auth_plugin(auth_options) + self.auth_plugin_name = auth.select_auth_plugin(self._cli_options) # Basic option checking to avoid unhelpful error messages - auth.check_valid_auth_options(auth_options, self.auth_plugin_name) + auth.check_valid_auth_options(self._cli_options, self.auth_plugin_name) # Horrible hack alert...must handle prompt for null password if # password auth is requested. if (self.auth_plugin_name.endswith('password') and - not auth_options.os_password): - auth_options.os_password = pw_func() + not self._cli_options.os_password): + self._cli_options.os_password = self._pw_callback() (auth_plugin, self._auth_params) = auth.build_auth_params( self.auth_plugin_name, - auth_options, + self._cli_options, ) - self._url = auth_options.os_url - self._region_name = auth_options.os_region_name - self._api_version = api_version - self._auth_ref = None - self.timing = auth_options.timing - - default_domain = auth_options.os_default_domain + default_domain = self._cli_options.os_default_domain # NOTE(stevemar): If PROJECT_DOMAIN_ID or PROJECT_DOMAIN_NAME is # present, then do not change the behaviour. Otherwise, set the # PROJECT_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. @@ -125,20 +153,6 @@ class ClientManager(object): elif 'tenant_name' in self._auth_params: self._project_name = self._auth_params['tenant_name'] - # verify is the Requests-compatible form - self._verify = verify - # also store in the form used by the legacy client libs - self._cacert = None - if isinstance(verify, bool): - self._insecure = not verify - else: - self._cacert = verify - self._insecure = False - - # Get logging from root logger - root_logger = logging.getLogger('') - LOG.setLevel(root_logger.getEffectiveLevel()) - LOG.info('Using auth plugin: %s' % self.auth_plugin_name) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication @@ -146,7 +160,7 @@ class ClientManager(object): self.session = session.Session( auth=self.auth, session=request_session, - verify=verify, + verify=self._verify, ) return @@ -155,6 +169,7 @@ class ClientManager(object): def auth_ref(self): """Dereference will trigger an auth if it hasn't already""" if not self._auth_ref: + self.setup_auth() LOG.debug("Get auth_ref") self._auth_ref = self.auth.get_auth_ref(self.session) return self._auth_ref diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 01a40e74..4139770c 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -21,7 +21,7 @@ import os import six import time -from oslo.utils import importutils +from oslo_utils import importutils from openstackclient.common import exceptions @@ -200,8 +200,14 @@ def sort_items(items, sort_str): reverse = False if ':' in sort_key: sort_key, direction = sort_key.split(':', 1) + if not sort_key: + msg = "empty string is not a valid sort key" + raise exceptions.CommandError(msg) if direction not in ['asc', 'desc']: - msg = "Specify sort direction by asc or desc" + if not direction: + direction = "empty string" + msg = ("%s is not a valid sort direction for sort key %s, " + "use asc or desc instead" % (direction, sort_key)) raise exceptions.CommandError(msg) if direction == 'desc': reverse = True diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 166747d5..7ca08a4f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -15,8 +15,13 @@ import logging +from novaclient import client as nova_client from novaclient import extension -from novaclient.v1_1.contrib import list_extensions + +try: + from novaclient.v2.contrib import list_extensions +except ImportError: + from novaclient.v1_1.contrib import list_extensions from openstackclient.common import utils @@ -25,19 +30,13 @@ LOG = logging.getLogger(__name__) DEFAULT_COMPUTE_API_VERSION = '2' API_VERSION_OPTION = 'os_compute_api_version' API_NAME = 'compute' -API_VERSIONS = { - '1.1': 'novaclient.v1_1.client.Client', - '1': 'novaclient.v1_1.client.Client', - '2': 'novaclient.v1_1.client.Client', -} def make_client(instance): """Returns a compute service client.""" - compute_client = utils.get_client_class( - API_NAME, + compute_client = nova_client.get_client_class( instance._api_version[API_NAME], - API_VERSIONS) + ) LOG.debug('Instantiating compute client: %s', compute_client) # Set client http_log_debug to True if verbosity level is high enough diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 195c9a0d..eb18a433 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -22,6 +22,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -237,8 +238,79 @@ class ShowFlavor(show.ShowOne): def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute - flavor = utils.find_resource(compute_client.flavors, - parsed_args.flavor)._info.copy() - flavor.pop("links") + resource_flavor = utils.find_resource(compute_client.flavors, + parsed_args.flavor) + flavor = resource_flavor._info.copy() + flavor.pop("links", None) + + flavor['properties'] = utils.format_dict(resource_flavor.get_keys()) + + return zip(*sorted(six.iteritems(flavor))) + + +class SetFlavor(show.ShowOne): + """Set flavor properties""" + + log = logging.getLogger(__name__ + ".SetFlavor") + + def get_parser(self, prog_name): + parser = super(SetFlavor, self).get_parser(prog_name) + parser.add_argument( + "--property", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help='Property to add or modify for this flavor ' + '(repeat option to set multiple properties)', + ) + parser.add_argument( + "flavor", + metavar="<flavor>", + help="Flavor to modify (name or ID)", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + compute_client = self.app.client_manager.compute + resource_flavor = compute_client.flavors.find(name=parsed_args.flavor) + + resource_flavor.set_keys(parsed_args.property) + + flavor = resource_flavor._info.copy() + flavor['properties'] = utils.format_dict(resource_flavor.get_keys()) + flavor.pop("links", None) + return zip(*sorted(six.iteritems(flavor))) + + +class UnsetFlavor(show.ShowOne): + """Unset flavor properties""" + + log = logging.getLogger(__name__ + ".UnsetFlavor") + + def get_parser(self, prog_name): + parser = super(UnsetFlavor, self).get_parser(prog_name) + parser.add_argument( + "--property", + metavar="<key>", + action='append', + help='Property to remove from flavor ' + '(repeat option to unset multiple properties)', + ) + parser.add_argument( + "flavor", + metavar="<flavor>", + help="Flavor to modify (name or ID)", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + compute_client = self.app.client_manager.compute + resource_flavor = compute_client.flavors.find(name=parsed_args.flavor) + + resource_flavor.unset_keys(parsed_args.property) + flavor = resource_flavor._info.copy() + flavor['properties'] = utils.format_dict(resource_flavor.get_keys()) + flavor.pop("links", None) return zip(*sorted(six.iteritems(flavor))) diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index e01258d1..65035d04 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -16,6 +16,7 @@ """Hypervisor action implementations""" import logging +import re import six from cliff import lister @@ -33,8 +34,8 @@ class ListHypervisor(lister.Lister): parser = super(ListHypervisor, self).get_parser(prog_name) parser.add_argument( "--matching", - metavar="<hostname-str>", - help="Filter hypervisors using <hostname-str> substring", + metavar="<hostname>", + help="Filter hypervisors using <hostname> substring", ) return parser @@ -58,23 +59,35 @@ class ListHypervisor(lister.Lister): class ShowHypervisor(show.ShowOne): - """Show hypervisor details""" + """Display hypervisor details""" log = logging.getLogger(__name__ + ".ShowHypervisor") def get_parser(self, prog_name): parser = super(ShowHypervisor, self).get_parser(prog_name) parser.add_argument( - "id", - metavar="<id>", - help="ID of the hypervisor to display") + "hypervisor", + metavar="<hypervisor>", + help="Hypervisor to display (name or ID)") return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute hypervisor = utils.find_resource(compute_client.hypervisors, - parsed_args.id)._info.copy() + parsed_args.hypervisor)._info.copy() + + uptime = compute_client.hypervisors.uptime(hypervisor['id'])._info + # Extract data from uptime value + # format: 0 up 0, 0 users, load average: 0, 0, 0 + # example: 17:37:14 up 2:33, 3 users, load average: 0.33, 0.36, 0.34 + m = re.match("(.+)\sup\s+(.+),\s+(.+)\susers,\s+load average:\s(.+)", + uptime['uptime']) + if m: + hypervisor["host_time"] = m.group(1) + hypervisor["uptime"] = m.group(2) + hypervisor["users"] = m.group(3) + hypervisor["load_average"] = m.group(4) hypervisor["service_id"] = hypervisor["service"]["id"] hypervisor["service_host"] = hypervisor["service"]["host"] diff --git a/openstackclient/compute/v2/hypervisor_stats.py b/openstackclient/compute/v2/hypervisor_stats.py new file mode 100644 index 00000000..43ba9fc8 --- /dev/null +++ b/openstackclient/compute/v2/hypervisor_stats.py @@ -0,0 +1,33 @@ +# 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. +# + + +"""Hypervisor Stats action implementations""" + +import logging +import six + +from cliff import show + + +class ShowHypervisorStats(show.ShowOne): + """Display hypervisor stats details""" + + log = logging.getLogger(__name__ + ".ShowHypervisorStats") + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + compute_client = self.app.client_manager.compute + hypervisor_stats = compute_client.hypervisors.statistics().to_dict() + + return zip(*sorted(six.iteritems(hypervisor_stats))) diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index f7ffb1d1..d4643438 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -24,7 +24,12 @@ from cliff import lister from cliff import show from keystoneclient import exceptions as ksc_exc -from novaclient.v1_1 import security_group_rules + +try: + from novaclient.v2 import security_group_rules +except ImportError: + from novaclient.v1_1 import security_group_rules + from openstackclient.common import parseractions from openstackclient.common import utils @@ -322,7 +327,7 @@ class DeleteSecurityGroupRule(command.Command): parser.add_argument( 'group', metavar='<group>', - help='Create rule in this security group', + help='Security group rule to delete (name or ID)', ) parser.add_argument( "--proto", @@ -375,7 +380,7 @@ class ListSecurityGroupRule(lister.Lister): parser.add_argument( 'group', metavar='<group>', - help='Create rule in this security group', + help='List all rules in this security group', ) return parser diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index f0a59d34..49ef18b2 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -26,7 +26,11 @@ import sys from cliff import command from cliff import lister from cliff import show -from novaclient.v1_1 import servers + +try: + from novaclient.v2 import servers +except ImportError: + from novaclient.v1_1 import servers from openstackclient.common import exceptions from openstackclient.common import parseractions @@ -1069,9 +1073,9 @@ class ResizeServer(command.Command): help=_('Resize server to specified flavor'), ) phase_group.add_argument( - '--verify', + '--confirm', action="store_true", - help=_('Verify server resize is complete'), + help=_('Confirm server resize is complete'), ) phase_group.add_argument( '--revert', @@ -1110,7 +1114,7 @@ class ResizeServer(command.Command): else: sys.stdout.write(_('\nError resizing server')) raise SystemExit - elif parsed_args.verify: + elif parsed_args.confirm: compute_client.servers.confirm_resize(server) elif parsed_args.revert: compute_client.servers.revert_resize(server) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index d10d046d..4127a451 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -46,7 +46,6 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating identity client: %s', identity_client) - LOG.debug('Using auth plugin: %s' % instance._auth_plugin) client = identity_client( session=instance.session, region_name=instance._region_name, diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index f6048378..7d17fbf5 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -26,8 +26,10 @@ from openstackclient.i18n import _ # noqa def _format_endpoints(eps=None): if not eps: return "" + ret = '' for index, ep in enumerate(eps): - ret = eps[index]['region'] + '\n' + region = eps[index].get('region', '<none>') + ret += region + '\n' for url in ['publicURL', 'internalURL', 'adminURL']: ret += " %s: %s\n" % (url, eps[index]['publicURL']) return ret diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py new file mode 100644 index 00000000..1899f25e --- /dev/null +++ b/openstackclient/identity/v3/catalog.py @@ -0,0 +1,100 @@ +# 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. +# + +"""Identity v3 Service Catalog action implementations""" + +import logging + +from cliff import lister +from cliff import show +import six + +from openstackclient.common import utils +from openstackclient.i18n import _ # noqa + + +def _format_endpoints(eps=None): + if not eps: + return "" + ret = '' + for ep in eps: + region = ep.get('region_id') or ep.get('region', '<none>') + ret += region + '\n' + ret += " %s: %s\n" % (ep['interface'], ep['url']) + return ret + + +class ListCatalog(lister.Lister): + """List services in the service catalog""" + + log = logging.getLogger(__name__ + '.ListCatalog') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + # This is ugly because if auth hasn't happened yet we need + # to trigger it here. + sc = self.app.client_manager.session.auth.get_auth_ref( + self.app.client_manager.session, + ).service_catalog + + data = sc.get_data() + columns = ('Name', 'Type', 'Endpoints') + return (columns, + (utils.get_dict_properties( + s, columns, + formatters={ + 'Endpoints': _format_endpoints, + }, + ) for s in data)) + + +class ShowCatalog(show.ShowOne): + """Display service catalog details""" + + log = logging.getLogger(__name__ + '.ShowCatalog') + + def get_parser(self, prog_name): + parser = super(ShowCatalog, self).get_parser(prog_name) + parser.add_argument( + 'service', + metavar='<service>', + help=_('Service to display (type or name)'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + # This is ugly because if auth hasn't happened yet we need + # to trigger it here. + sc = self.app.client_manager.session.auth.get_auth_ref( + self.app.client_manager.session, + ).service_catalog + + data = None + for service in sc.get_data(): + if (service.get('name') == parsed_args.service or + service.get('type') == parsed_args.service): + data = dict(service) + data['endpoints'] = _format_endpoints(data['endpoints']) + if 'links' in data: + data.pop('links') + break + + if not data: + self.app.log.error('service %s not found\n' % + parsed_args.service) + return ([], []) + + return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 246f51b1..3cfd7312 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -22,7 +22,6 @@ import traceback from cliff import app from cliff import command -from cliff import complete from cliff import help import openstackclient @@ -70,10 +69,9 @@ class OpenStackShell(app.App): def __init__(self): # Patch command.Command to add a default auth_required = True command.Command.auth_required = True - command.Command.best_effort = False - # But not help + + # Some commands do not need authentication help.HelpCommand.auth_required = False - complete.CompleteCommand.best_effort = True super(OpenStackShell, self).__init__( description=__doc__.strip(), @@ -190,12 +188,6 @@ class OpenStackShell(app.App): description, version) - # service token auth argument - parser.add_argument( - '--os-url', - metavar='<url>', - default=utils.env('OS_URL'), - help='Defaults to env[OS_URL]') # Global arguments parser.add_argument( '--os-region-name', @@ -294,7 +286,7 @@ class OpenStackShell(app.App): self.verify = not self.options.insecure self.client_manager = clientmanager.ClientManager( - auth_options=self.options, + cli_options=self.options, verify=self.verify, api_version=self.api_version, pw_func=prompt_for_password, @@ -308,7 +300,7 @@ class OpenStackShell(app.App): cmd.__class__.__module__, cmd.__class__.__name__, ) - if cmd.auth_required and cmd.best_effort: + if cmd.auth_required: try: # Trigger the Identity client to initialize self.client_manager.auth_ref diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 3b2b976b..3648bf57 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -17,9 +17,10 @@ from requests_mock.contrib import fixture from keystoneclient.auth.identity import v2 as auth_v2 from keystoneclient import service_catalog -from oslo.serialization import jsonutils +from oslo_serialization import jsonutils from openstackclient.api import auth +from openstackclient.api import auth_plugin from openstackclient.common import clientmanager from openstackclient.common import exceptions as exc from openstackclient.tests import fakes @@ -80,12 +81,16 @@ class TestClientManager(utils.TestCase): def test_client_manager_token_endpoint(self): client_manager = clientmanager.ClientManager( - auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, - os_url=fakes.AUTH_URL, - os_auth_type='token_endpoint'), + cli_options=FakeOptions( + os_token=fakes.AUTH_TOKEN, + os_url=fakes.AUTH_URL, + os_auth_type='token_endpoint', + ), api_version=API_VERSION, verify=True ) + client_manager.setup_auth() + self.assertEqual( fakes.AUTH_URL, client_manager._url, @@ -96,7 +101,7 @@ class TestClientManager(utils.TestCase): ) self.assertIsInstance( client_manager.auth, - auth.TokenEndpoint, + auth_plugin.TokenEndpoint, ) self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) @@ -104,12 +109,15 @@ class TestClientManager(utils.TestCase): def test_client_manager_token(self): client_manager = clientmanager.ClientManager( - auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, - os_auth_url=fakes.AUTH_URL, - os_auth_type='v2token'), + cli_options=FakeOptions( + os_token=fakes.AUTH_TOKEN, + os_auth_url=fakes.AUTH_URL, + os_auth_type='v2token', + ), api_version=API_VERSION, verify=True ) + client_manager.setup_auth() self.assertEqual( fakes.AUTH_URL, @@ -125,13 +133,16 @@ class TestClientManager(utils.TestCase): def test_client_manager_password(self): client_manager = clientmanager.ClientManager( - auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, - os_username=fakes.USERNAME, - os_password=fakes.PASSWORD, - os_project_name=fakes.PROJECT_NAME), + cli_options=FakeOptions( + os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD, + os_project_name=fakes.PROJECT_NAME, + ), api_version=API_VERSION, verify=False, ) + client_manager.setup_auth() self.assertEqual( fakes.AUTH_URL, @@ -182,14 +193,17 @@ class TestClientManager(utils.TestCase): def test_client_manager_password_verify_ca(self): client_manager = clientmanager.ClientManager( - auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, - os_username=fakes.USERNAME, - os_password=fakes.PASSWORD, - os_project_name=fakes.PROJECT_NAME, - os_auth_type='v2password'), + cli_options=FakeOptions( + os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD, + os_project_name=fakes.PROJECT_NAME, + os_auth_type='v2password', + ), api_version=API_VERSION, verify='cafile', ) + client_manager.setup_auth() self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) @@ -199,10 +213,12 @@ class TestClientManager(utils.TestCase): auth_params['os_auth_type'] = auth_plugin_name auth_params['os_identity_api_version'] = api_version client_manager = clientmanager.ClientManager( - auth_options=FakeOptions(**auth_params), + cli_options=FakeOptions(**auth_params), api_version=API_VERSION, verify=True ) + client_manager.setup_auth() + self.assertEqual( auth_plugin_name, client_manager.auth_plugin_name, @@ -228,8 +244,12 @@ class TestClientManager(utils.TestCase): self._select_auth_plugin(params, 'XXX', 'password') def test_client_manager_select_auth_plugin_failure(self): - self.assertRaises(exc.CommandError, - clientmanager.ClientManager, - auth_options=FakeOptions(os_auth_plugin=''), - api_version=API_VERSION, - verify=True) + client_manager = clientmanager.ClientManager( + cli_options=FakeOptions(os_auth_plugin=''), + api_version=API_VERSION, + verify=True, + ) + self.assertRaises( + exc.CommandError, + client_manager.setup_auth, + ) diff --git a/openstackclient/tests/compute/v2/test_flavor.py b/openstackclient/tests/compute/v2/test_flavor.py index 8f33ccfe..19be8124 100644 --- a/openstackclient/tests/compute/v2/test_flavor.py +++ b/openstackclient/tests/compute/v2/test_flavor.py @@ -22,8 +22,17 @@ from openstackclient.tests import fakes class FakeFlavorResource(fakes.FakeResource): + _keys = {'property': 'value'} + + def set_keys(self, args): + self._keys.update(args) + + def unset_keys(self, keys): + for key in keys: + self._keys.pop(key, None) + def get_keys(self): - return {'property': 'value'} + return self._keys class TestFlavor(compute_fakes.TestComputev2): @@ -272,3 +281,69 @@ class TestFlavorList(TestFlavor): 'property=\'value\'' ), ) self.assertEqual(datalist, tuple(data)) + + +class TestFlavorSet(TestFlavor): + + def setUp(self): + super(TestFlavorSet, self).setUp() + + self.flavors_mock.find.return_value = FakeFlavorResource( + None, + copy.deepcopy(compute_fakes.FLAVOR), + loaded=True, + ) + + self.cmd = flavor.SetFlavor(self.app, None) + + def test_flavor_set(self): + arglist = [ + '--property', 'FOO="B A R"', + 'baremetal' + ] + verifylist = [ + ('property', {'FOO': '"B A R"'}), + ('flavor', 'baremetal') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.flavors_mock.find.assert_called_with(name='baremetal') + + self.assertEqual('properties', columns[2]) + self.assertIn('FOO=\'"B A R"\'', data[2]) + + +class TestFlavorUnset(TestFlavor): + + def setUp(self): + super(TestFlavorUnset, self).setUp() + + self.flavors_mock.find.return_value = FakeFlavorResource( + None, + copy.deepcopy(compute_fakes.FLAVOR), + loaded=True, + ) + + self.cmd = flavor.UnsetFlavor(self.app, None) + + def test_flavor_unset(self): + arglist = [ + '--property', 'property', + 'baremetal' + ] + verifylist = [ + ('property', ['property']), + ('flavor', 'baremetal'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.flavors_mock.find.assert_called_with(name='baremetal') + + self.assertEqual('properties', columns[2]) + self.assertNotIn('property', data[2]) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 5a126697..079f301e 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -437,7 +437,7 @@ class TestServerResize(TestServer): compute_fakes.server_id, ] verifylist = [ - ('verify', False), + ('confirm', False), ('revert', False), ('server', compute_fakes.server_id), ] @@ -461,7 +461,7 @@ class TestServerResize(TestServer): ] verifylist = [ ('flavor', compute_fakes.flavor_id), - ('verify', False), + ('confirm', False), ('revert', False), ('server', compute_fakes.server_id), ] @@ -486,11 +486,11 @@ class TestServerResize(TestServer): def test_server_resize_confirm(self): arglist = [ - '--verify', + '--confirm', compute_fakes.server_id, ] verifylist = [ - ('verify', True), + ('confirm', True), ('revert', False), ('server', compute_fakes.server_id), ] @@ -515,7 +515,7 @@ class TestServerResize(TestServer): compute_fakes.server_id, ] verifylist = [ - ('verify', False), + ('confirm', False), ('revert', True), ('server', compute_fakes.server_id), ] diff --git a/openstackclient/tests/identity/v2_0/test_catalog.py b/openstackclient/tests/identity/v2_0/test_catalog.py index 5289cac4..50445954 100644 --- a/openstackclient/tests/identity/v2_0/test_catalog.py +++ b/openstackclient/tests/identity/v2_0/test_catalog.py @@ -23,11 +23,18 @@ class TestCatalog(utils.TestCommand): 'id': 'qwertyuiop', 'type': 'compute', 'name': 'supernova', - 'endpoints': [{ - 'region': 'onlyone', - 'publicURL': 'https://public.example.com', - 'adminURL': 'https://admin.example.com', - }], + 'endpoints': [ + { + 'region': 'one', + 'publicURL': 'https://public.one.example.com', + 'adminURL': 'https://admin.one.example.com', + }, + { + 'region': 'two', + 'publicURL': 'https://public.two.example.com', + 'adminURL': 'https://admin.two.example.com', + }, + ], } def setUp(self): @@ -66,9 +73,12 @@ class TestCatalogList(TestCatalog): datalist = (( 'supernova', 'compute', - 'onlyone\n publicURL: https://public.example.com\n ' - 'internalURL: https://public.example.com\n ' - 'adminURL: https://public.example.com\n', + 'one\n publicURL: https://public.one.example.com\n ' + 'internalURL: https://public.one.example.com\n ' + 'adminURL: https://public.one.example.com\n' + 'two\n publicURL: https://public.two.example.com\n ' + 'internalURL: https://public.two.example.com\n ' + 'adminURL: https://public.two.example.com\n', ), ) self.assertEqual(datalist, tuple(data)) @@ -97,9 +107,12 @@ class TestCatalogShow(TestCatalog): collist = ('endpoints', 'id', 'name', 'type') self.assertEqual(collist, columns) datalist = ( - 'onlyone\n publicURL: https://public.example.com\n ' - 'internalURL: https://public.example.com\n ' - 'adminURL: https://public.example.com\n', + 'one\n publicURL: https://public.one.example.com\n ' + 'internalURL: https://public.one.example.com\n ' + 'adminURL: https://public.one.example.com\n' + 'two\n publicURL: https://public.two.example.com\n ' + 'internalURL: https://public.two.example.com\n ' + 'adminURL: https://public.two.example.com\n', 'qwertyuiop', 'supernova', 'compute', diff --git a/openstackclient/tests/identity/v3/test_catalog.py b/openstackclient/tests/identity/v3/test_catalog.py new file mode 100644 index 00000000..6bb962de --- /dev/null +++ b/openstackclient/tests/identity/v3/test_catalog.py @@ -0,0 +1,118 @@ +# 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 mock + +from openstackclient.identity.v3 import catalog +from openstackclient.tests import utils + + +class TestCatalog(utils.TestCommand): + + fake_service = { + 'id': 'qwertyuiop', + 'type': 'compute', + 'name': 'supernova', + 'endpoints': [ + { + 'region': 'onlyone', + 'url': 'https://public.example.com', + 'interface': 'public', + }, + { + 'region_id': 'onlyone', + 'url': 'https://admin.example.com', + 'interface': 'admin', + }, + { + 'url': 'https://internal.example.com', + 'interface': 'internal', + }, + ], + } + + def setUp(self): + super(TestCatalog, self).setUp() + + self.sc_mock = mock.MagicMock() + self.sc_mock.service_catalog.get_data.return_value = [ + self.fake_service, + ] + + self.auth_mock = mock.MagicMock() + self.app.client_manager.session = self.auth_mock + + self.auth_mock.auth.get_auth_ref.return_value = self.sc_mock + + +class TestCatalogList(TestCatalog): + + def setUp(self): + super(TestCatalogList, self).setUp() + + # Get the command object to test + self.cmd = catalog.ListCatalog(self.app, None) + + def test_catalog_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.sc_mock.service_catalog.get_data.assert_called_with() + + collist = ('Name', 'Type', 'Endpoints') + self.assertEqual(collist, columns) + datalist = (( + 'supernova', + 'compute', + 'onlyone\n public: https://public.example.com\n' + 'onlyone\n admin: https://admin.example.com\n' + '<none>\n internal: https://internal.example.com\n', + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestCatalogShow(TestCatalog): + + def setUp(self): + super(TestCatalogShow, self).setUp() + + # Get the command object to test + self.cmd = catalog.ShowCatalog(self.app, None) + + def test_catalog_show(self): + arglist = [ + 'compute', + ] + verifylist = [ + ('service', 'compute'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.sc_mock.service_catalog.get_data.assert_called_with() + + collist = ('endpoints', 'id', 'name', 'type') + self.assertEqual(collist, columns) + datalist = ( + 'onlyone\n public: https://public.example.com\nonlyone\n' + ' admin: https://admin.example.com\n' + '<none>\n internal: https://internal.example.com\n', + 'qwertyuiop', + 'supernova', + 'compute', + ) + self.assertEqual(datalist, data) |
