summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/api/auth.py103
-rw-r--r--openstackclient/api/auth_plugin.py107
-rw-r--r--openstackclient/api/image_v1.py9
-rw-r--r--openstackclient/api/image_v2.py7
-rw-r--r--openstackclient/common/clientmanager.py73
-rw-r--r--openstackclient/common/utils.py10
-rw-r--r--openstackclient/compute/client.py17
-rw-r--r--openstackclient/compute/v2/flavor.py78
-rw-r--r--openstackclient/compute/v2/hypervisor.py27
-rw-r--r--openstackclient/compute/v2/hypervisor_stats.py33
-rw-r--r--openstackclient/compute/v2/security_group.py11
-rw-r--r--openstackclient/compute/v2/server.py12
-rw-r--r--openstackclient/identity/client.py1
-rw-r--r--openstackclient/identity/v2_0/catalog.py4
-rw-r--r--openstackclient/identity/v3/catalog.py100
-rw-r--r--openstackclient/shell.py16
-rw-r--r--openstackclient/tests/common/test_clientmanager.py66
-rw-r--r--openstackclient/tests/compute/v2/test_flavor.py77
-rw-r--r--openstackclient/tests/compute/v2/test_server.py10
-rw-r--r--openstackclient/tests/identity/v2_0/test_catalog.py35
-rw-r--r--openstackclient/tests/identity/v3/test_catalog.py118
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)