diff options
Diffstat (limited to 'openstackclient')
19 files changed, 1295 insertions, 489 deletions
diff --git a/openstackclient/api/utils.py b/openstackclient/api/utils.py deleted file mode 100644 index 6407cd44..00000000 --- a/openstackclient/api/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -# 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. -# - -"""API Utilities Library""" - - -def simple_filter( - data=None, - attr=None, - value=None, - property_field=None, -): - """Filter a list of dicts - - :param list data: - The list to be filtered. The list is modified in-place and will - be changed if any filtering occurs. - :param string attr: - The name of the attribute to filter. If attr does not exist no - match will succeed and no rows will be returned. If attr is - None no filtering will be performed and all rows will be returned. - :param string value: - The value to filter. None is considered to be a 'no filter' value. - '' matches against a Python empty string. - :param string property_field: - The name of the data field containing a property dict to filter. - If property_field is None, attr is a field name. If property_field - is not None, attr is a property key name inside the named property - field. - - :returns: - Returns the filtered list - :rtype list: - - This simple filter (one attribute, one exact-match value) searches a - list of dicts to select items. It first searches the item dict for a - matching ``attr`` then does an exact-match on the ``value``. If - ``property_field`` is given, it will look inside that field (if it - exists and is a dict) for a matching ``value``. - """ - - # Take the do-nothing case shortcut - if not data or not attr or value is None: - return data - - # NOTE:(dtroyer): This filter modifies the provided list in-place using - # list.remove() so we need to start at the end so the loop pointer does - # not skip any items after a deletion. - for d in reversed(data): - if attr in d: - # Searching data fields - search_value = d[attr] - elif (property_field and property_field in d and - isinstance(d[property_field], dict)): - # Searching a properties field - do this separately because - # we don't want to fail over to checking the fields if a - # property name is given. - if attr in d[property_field]: - search_value = d[property_field][attr] - else: - search_value = None - else: - search_value = None - - # could do regex here someday... - if not search_value or search_value != value: - # remove from list - try: - data.remove(d) - except ValueError: - # it's already gone! - pass - - return data diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index aa1045e4..c1118ad3 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -91,13 +91,6 @@ class ClientManager(clientmanager.ClientManager): return super(ClientManager, self).setup_auth() - @property - def auth_ref(self): - if not self._auth_required: - return None - else: - return super(ClientManager, self).auth_ref - def _fallback_load_auth_plugin(self, e): # NOTES(RuiChen): Hack to avoid auth plugins choking on data they don't # expect, delete fake token and endpoint, then try to diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py deleted file mode 100644 index c190e33e..00000000 --- a/openstackclient/common/commandmanager.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2012-2013 OpenStack Foundation -# -# 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. -# - -"""Modify cliff.CommandManager""" - -import pkg_resources - -import cliff.commandmanager - - -class CommandManager(cliff.commandmanager.CommandManager): - """Add additional functionality to cliff.CommandManager - - Load additional command groups after initialization - Add _command_group() methods - """ - - def __init__(self, namespace, convert_underscores=True): - self.group_list = [] - super(CommandManager, self).__init__(namespace, convert_underscores) - - def load_commands(self, namespace): - self.group_list.append(namespace) - return super(CommandManager, self).load_commands(namespace) - - def add_command_group(self, group=None): - """Adds another group of command entrypoints""" - if group: - self.load_commands(group) - - def get_command_groups(self): - """Returns a list of the loaded command groups""" - return self.group_list - - def get_command_names(self, group=None): - """Returns a list of commands loaded for the specified group""" - group_list = [] - if group is not None: - for ep in pkg_resources.iter_entry_points(group): - cmd_name = ( - ep.name.replace('_', ' ') - if self.convert_underscores - else ep.name - ) - group_list.append(cmd_name) - return group_list - return list(self.commands.keys()) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 7f9161a9..fa646478 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -101,6 +101,8 @@ class CreateAggregate(command.ShowOne): parsed_args.property, )._info) + # TODO(dtroyer): re-format metadata field to properites as + # in the set command return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index cb9f8d43..3e1deed5 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -540,6 +540,12 @@ class CreateServer(command.ShowOne): help=_('User data file to serve from the metadata server'), ) parser.add_argument( + '--description', + metavar='<description>', + help=_('Set description for the server (supported by ' + '--os-compute-api-version 2.19 or above)'), + ) + parser.add_argument( '--availability-zone', metavar='<zone-name>', help=_('Select an availability zone for the server'), @@ -749,6 +755,12 @@ class CreateServer(command.ShowOne): "exception": e} ) + if parsed_args.description: + if compute_client.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + block_device_mapping_v2 = [] if volume: block_device_mapping_v2 = [{'uuid': volume, @@ -909,6 +921,9 @@ class CreateServer(command.ShowOne): scheduler_hints=hints, config_drive=config_drive) + if parsed_args.description: + boot_kwargs['description'] = parsed_args.description + LOG.debug('boot_args: %s', boot_args) LOG.debug('boot_kwargs: %s', boot_kwargs) @@ -1129,12 +1144,21 @@ class ListServer(command.Lister): help=_('Only display deleted servers (Admin only).') ) parser.add_argument( + '--changes-before', + metavar='<changes-before>', + default=None, + help=_("List only servers changed before a certain point of time. " + "The provided time should be an ISO 8061 formatted time " + "(e.g., 2016-03-05T06:27:59Z). " + "(Supported by API versions '2.66' - '2.latest')") + ) + parser.add_argument( '--changes-since', metavar='<changes-since>', default=None, help=_("List only servers changed after a certain point of time." - " The provided time should be an ISO 8061 formatted time." - " ex 2016-03-04T06:27:59Z .") + " The provided time should be an ISO 8061 formatted time" + " (e.g., 2016-03-04T06:27:59Z).") ) return parser @@ -1188,10 +1212,24 @@ class ListServer(command.Lister): 'all_tenants': parsed_args.all_projects, 'user_id': user_id, 'deleted': parsed_args.deleted, + 'changes-before': parsed_args.changes_before, 'changes-since': parsed_args.changes_since, } LOG.debug('search options: %s', search_opts) + if search_opts['changes-before']: + if compute_client.api_version < api_versions.APIVersion('2.66'): + msg = _('--os-compute-api-version 2.66 or later is required') + raise exceptions.CommandError(msg) + + try: + timeutils.parse_isotime(search_opts['changes-before']) + except ValueError: + raise exceptions.CommandError( + _('Invalid changes-before value: %s') % + search_opts['changes-before'] + ) + if search_opts['changes-since']: try: timeutils.parse_isotime(search_opts['changes-since']) @@ -1407,9 +1445,38 @@ class MigrateServer(command.Command): help=_('Server (name or ID)'), ) parser.add_argument( + '--live-migration', + dest='live_migration', + action='store_true', + help=_('Live migrate the server. Use the ``--host`` option to ' + 'specify a target host for the migration which will be ' + 'validated by the scheduler.'), + ) + # The --live and --host options are mutually exclusive ways of asking + # for a target host during a live migration. + host_group = parser.add_mutually_exclusive_group() + # TODO(mriedem): Remove --live in the next major version bump after + # the Train release. + host_group.add_argument( '--live', metavar='<hostname>', - help=_('Target hostname'), + help=_('**Deprecated** This option is problematic in that it ' + 'requires a host and prior to compute API version 2.30, ' + 'specifying a host during live migration will bypass ' + 'validation by the scheduler which could result in ' + 'failures to actually migrate the server to the specified ' + 'host or over-subscribe the host. Use the ' + '``--live-migration`` option instead. If both this option ' + 'and ``--live-migration`` are used, ``--live-migration`` ' + 'takes priority.'), + ) + # TODO(mriedem): Add support for --os-compute-api-version >= 2.56 where + # you can cold migrate to a specified target host. + host_group.add_argument( + '--host', + metavar='<hostname>', + help=_('Live migrate the server to the specified host. Requires ' + '``--os-compute-api-version`` 2.30 or greater.'), ) migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( @@ -1447,6 +1514,15 @@ class MigrateServer(command.Command): ) return parser + def _log_warning_for_live(self, parsed_args): + if parsed_args.live: + # NOTE(mriedem): The --live option requires a host and if + # --os-compute-api-version is less than 2.30 it will forcefully + # bypass the scheduler which is dangerous. + self.log.warning(_( + 'The --live option has been deprecated. Please use the ' + '--live-migration option instead.')) + def take_action(self, parsed_args): def _show_progress(progress): @@ -1460,19 +1536,45 @@ class MigrateServer(command.Command): compute_client.servers, parsed_args.server, ) - if parsed_args.live: + # Check for live migration. + if parsed_args.live or parsed_args.live_migration: + # Always log a warning if --live is used. + self._log_warning_for_live(parsed_args) kwargs = { - 'host': parsed_args.live, 'block_migration': parsed_args.block_migration } + # Prefer --live-migration over --live if both are specified. + if parsed_args.live_migration: + # Technically we could pass a non-None host with + # --os-compute-api-version < 2.30 but that is the same thing + # as the --live option bypassing the scheduler which we don't + # want to support, so if the user is using --live-migration + # and --host, we want to enforce that they are using version + # 2.30 or greater. + if (parsed_args.host and + compute_client.api_version < + api_versions.APIVersion('2.30')): + raise exceptions.CommandError( + '--os-compute-api-version 2.30 or greater is required ' + 'when using --host') + # The host parameter is required in the API even if None. + kwargs['host'] = parsed_args.host + else: + kwargs['host'] = parsed_args.live + if compute_client.api_version < api_versions.APIVersion('2.25'): kwargs['disk_over_commit'] = parsed_args.disk_overcommit server.live_migrate(**kwargs) else: - if parsed_args.block_migration or parsed_args.disk_overcommit: - raise exceptions.CommandError("--live must be specified if " - "--block-migration or " - "--disk-overcommit is specified") + if (parsed_args.block_migration or parsed_args.disk_overcommit or + parsed_args.host): + # TODO(mriedem): Allow --host for cold migration if + # --os-compute-api-version >= 2.56. + raise exceptions.CommandError( + "--live-migration must be specified if " + "--block-migration, --disk-overcommit or --host is " + "specified") + server.migrate() if parsed_args.wait: @@ -1601,6 +1703,12 @@ class RebuildServer(command.ShowOne): '(repeat option to set multiple values)'), ) parser.add_argument( + '--description', + metavar='<description>', + help=_('New description for the server (supported by ' + '--os-compute-api-version 2.19 or above'), + ) + parser.add_argument( '--wait', action='store_true', help=_('Wait for rebuild to complete'), @@ -1644,6 +1752,12 @@ class RebuildServer(command.ShowOne): kwargs = {} if parsed_args.property: kwargs['meta'] = parsed_args.property + if parsed_args.description: + if server.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + kwargs['description'] = parsed_args.description if parsed_args.key_name or parsed_args.key_unset: if compute_client.api_version < api_versions.APIVersion('2.54'): @@ -2065,6 +2179,12 @@ class SetServer(command.Command): choices=['active', 'error'], help=_('New server state (valid value: active, error)'), ) + parser.add_argument( + '--description', + metavar='<description>', + help=_('New server description (supported by ' + '--os-compute-api-version 2.19 or above)'), + ) return parser def take_action(self, parsed_args): @@ -2096,6 +2216,13 @@ class SetServer(command.Command): msg = _("Passwords do not match, password unchanged") raise exceptions.CommandError(msg) + if parsed_args.description: + if server.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + server.update(description=parsed_args.description) + class ShelveServer(command.Command): _description = _("Shelve server(s)") @@ -2455,6 +2582,13 @@ class UnsetServer(command.Command): help=_('Property key to remove from server ' '(repeat option to remove multiple values)'), ) + parser.add_argument( + '--description', + dest='description', + action='store_true', + help=_('Unset server description (supported by ' + '--os-compute-api-version 2.19 or above)'), + ) return parser def take_action(self, parsed_args): @@ -2470,6 +2604,16 @@ class UnsetServer(command.Command): parsed_args.property, ) + if parsed_args.description: + if compute_client.api_version < api_versions.APIVersion("2.19"): + msg = _("Description is not supported for " + "--os-compute-api-version less than 2.19") + raise exceptions.CommandError(msg) + compute_client.servers.update( + server, + description="", + ) + class UnshelveServer(command.Command): _description = _("Unshelve server(s)") diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 04d422ec..9bb5fc4d 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -289,7 +289,7 @@ class ShowProject(command.ShowOne): # the API has and handle the extra top level properties. reserved = ('name', 'id', 'enabled', 'description') properties = {} - for k, v in info.items(): + for k, v in list(info.items()): if k not in reserved: # If a key is not in `reserved` it's a property, pop it info.pop(k) diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py index e254973b..66bd164d 100644 --- a/openstackclient/identity/v3/endpoint_group.py +++ b/openstackclient/identity/v3/endpoint_group.py @@ -204,11 +204,11 @@ class ListEndpointGroup(command.Lister): if endpointgroup: # List projects associated to the endpoint group - columns = ('ID', 'Name') + columns = ('ID', 'Name', 'Description') data = client.endpoint_filter.list_projects_for_endpoint_group( endpoint_group=endpointgroup.id) elif project: - columns = ('ID', 'Name') + columns = ('ID', 'Name', 'Description') data = client.endpoint_filter.list_endpoint_groups_for_project( project=project.id) else: @@ -251,7 +251,7 @@ class RemoveProjectFromEndpointGroup(command.Command): parsed_args.project, parsed_args.project_domain) - client.endpoint_filter.delete_endpoint_group_to_project( + client.endpoint_filter.delete_endpoint_group_from_project( endpoint_group=endpointgroup.id, project=project.id) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 7ecaa3ef..64c4049c 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -22,12 +22,12 @@ import os import sys from glanceclient.common import utils as gc_utils +from osc_lib.api import utils as api_utils from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import utils import six -from openstackclient.api import utils as api_utils from openstackclient.i18n import _ if os.name == "nt": diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 3efde808..bdec99d7 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -21,13 +21,13 @@ import logging from glanceclient.common import utils as gc_utils from openstack.image import image_signer +from osc_lib.api import utils as api_utils from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils import six -from openstackclient.api import utils as api_utils from openstackclient.i18n import _ from openstackclient.identity import common diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 58a77120..22d8412c 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -20,13 +20,13 @@ import locale import sys from osc_lib.api import auth +from osc_lib.command import commandmanager from osc_lib import shell import six import openstackclient from openstackclient.common import client_config as cloud_config from openstackclient.common import clientmanager -from openstackclient.common import commandmanager DEFAULT_DOMAIN = 'default' diff --git a/openstackclient/tests/functional/compute/v2/test_aggregate.py b/openstackclient/tests/functional/compute/v2/test_aggregate.py index 590d28cb..c591dd61 100644 --- a/openstackclient/tests/functional/compute/v2/test_aggregate.py +++ b/openstackclient/tests/functional/compute/v2/test_aggregate.py @@ -11,7 +11,6 @@ # under the License. import json -import time import uuid from openstackclient.tests.functional import base @@ -20,12 +19,13 @@ from openstackclient.tests.functional import base class AggregateTests(base.TestCase): """Functional tests for aggregate""" - def test_aggregate_create_and_delete(self): + def test_aggregate_crud(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'aggregate create -f json ' + '--zone nova ' + + '--property a=b ' + name1 )) self.assertEqual( @@ -36,11 +36,17 @@ class AggregateTests(base.TestCase): 'nova', cmd_output['availability_zone'] ) + # TODO(dtroyer): enable the following once the properties field + # is correctly formatted in the create output + # self.assertIn( + # "a='b'", + # cmd_output['properties'] + # ) name2 = uuid.uuid4().hex cmd_output = json.loads(self.openstack( 'aggregate create -f json ' + - '--zone nova ' + + '--zone external ' + name2 )) self.assertEqual( @@ -48,102 +54,15 @@ class AggregateTests(base.TestCase): cmd_output['name'] ) self.assertEqual( - 'nova', + 'external', cmd_output['availability_zone'] ) - # Loop a few times since this is timing-sensitive - # Just hard-code it for now, since there is no pause and it is - # racy we shouldn't have to wait too long, a minute or two - # seems reasonable - wait_time = 0 - while wait_time <= 120: - cmd_output = json.loads(self.openstack( - 'aggregate show -f json ' + - name2 - )) - if cmd_output['name'] != name2: - # Hang out for a bit and try again - print('retrying aggregate check') - wait_time += 10 - time.sleep(10) - else: - break - - del_output = self.openstack( - 'aggregate delete ' + - name1 + ' ' + - name2 - ) - self.assertOutput('', del_output) - - def test_aggregate_list(self): - """Test aggregate list""" - name1 = uuid.uuid4().hex - self.openstack( - 'aggregate create ' + - '--zone nova ' + - '--property a=b ' + - name1 - ) - self.addCleanup( - self.openstack, - 'aggregate delete ' + name1, - fail_ok=True, - ) - - name2 = uuid.uuid4().hex - self.openstack( - 'aggregate create ' + - '--zone internal ' + - '--property c=d ' + - name2 - ) - self.addCleanup( - self.openstack, - 'aggregate delete ' + name2, - fail_ok=True, - ) - - cmd_output = json.loads(self.openstack( - 'aggregate list -f json' - )) - names = [x['Name'] for x in cmd_output] - self.assertIn(name1, names) - self.assertIn(name2, names) - zones = [x['Availability Zone'] for x in cmd_output] - self.assertIn('nova', zones) - self.assertIn('internal', zones) - - # Test aggregate list --long - cmd_output = json.loads(self.openstack( - 'aggregate list --long -f json' - )) - names = [x['Name'] for x in cmd_output] - self.assertIn(name1, names) - self.assertIn(name2, names) - zones = [x['Availability Zone'] for x in cmd_output] - self.assertIn('nova', zones) - self.assertIn('internal', zones) - properties = [x['Properties'] for x in cmd_output] - self.assertIn({'a': 'b'}, properties) - self.assertIn({'c': 'd'}, properties) - - def test_aggregate_set_and_unset(self): - """Test aggregate set, show and unset""" - name1 = uuid.uuid4().hex - name2 = uuid.uuid4().hex - self.openstack( - 'aggregate create ' + - '--zone nova ' + - '--property a=b ' + - name1 - ) - self.addCleanup(self.openstack, 'aggregate delete ' + name2) - + # Test aggregate set + name3 = uuid.uuid4().hex raw_output = self.openstack( 'aggregate set ' + - '--name ' + name2 + ' ' + + '--name ' + name3 + ' ' + '--zone internal ' + '--no-property ' + '--property c=d ' + @@ -153,10 +72,10 @@ class AggregateTests(base.TestCase): cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + - name2 + name3 )) self.assertEqual( - name2, + name3, cmd_output['name'] ) self.assertEqual( @@ -172,23 +91,56 @@ class AggregateTests(base.TestCase): cmd_output['properties'] ) + # Test aggregate list + cmd_output = json.loads(self.openstack( + 'aggregate list -f json' + )) + names = [x['Name'] for x in cmd_output] + self.assertIn(name3, names) + self.assertIn(name2, names) + zones = [x['Availability Zone'] for x in cmd_output] + self.assertIn('external', zones) + self.assertIn('internal', zones) + + # Test aggregate list --long + cmd_output = json.loads(self.openstack( + 'aggregate list --long -f json' + )) + names = [x['Name'] for x in cmd_output] + self.assertIn(name3, names) + self.assertIn(name2, names) + zones = [x['Availability Zone'] for x in cmd_output] + self.assertIn('external', zones) + self.assertIn('internal', zones) + properties = [x['Properties'] for x in cmd_output] + self.assertNotIn({'a': 'b'}, properties) + self.assertIn({'c': 'd'}, properties) + # Test unset raw_output = self.openstack( 'aggregate unset ' + '--property c ' + - name2 + name3 ) self.assertOutput('', raw_output) cmd_output = json.loads(self.openstack( 'aggregate show -f json ' + - name2 + name3 )) self.assertNotIn( "c='d'", cmd_output['properties'] ) + # test aggregate delete + del_output = self.openstack( + 'aggregate delete ' + + name3 + ' ' + + name2 + ) + self.assertOutput('', del_output) + def test_aggregate_add_and_remove_host(self): """Test aggregate add and remove host""" # Get a host diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index c8fb44d3..6330ac98 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -63,6 +63,84 @@ class ServerTests(common.ComputeTestCase): self.assertNotIn(name1, col_name) self.assertIn(name2, col_name) + def test_server_list_with_changes_before(self): + """Test server list. + + Getting the servers list with updated_at time equal or + before than changes-before. + """ + cmd_output = self.server_create() + server_name1 = cmd_output['name'] + + cmd_output = self.server_create() + server_name2 = cmd_output['name'] + updated_at2 = cmd_output['updated'] + + cmd_output = self.server_create() + server_name3 = cmd_output['name'] + + cmd_output = json.loads(self.openstack( + '--os-compute-api-version 2.66 ' + + 'server list -f json ' + '--changes-before ' + updated_at2 + )) + + col_updated = [server["Name"] for server in cmd_output] + self.assertIn(server_name1, col_updated) + self.assertIn(server_name2, col_updated) + self.assertNotIn(server_name3, col_updated) + + def test_server_list_with_changes_since(self): + """Test server list. + + Getting the servers list with updated_at time equal or + later than changes-since. + """ + cmd_output = self.server_create() + server_name1 = cmd_output['name'] + cmd_output = self.server_create() + server_name2 = cmd_output['name'] + updated_at2 = cmd_output['updated'] + cmd_output = self.server_create() + server_name3 = cmd_output['name'] + + cmd_output = json.loads(self.openstack( + 'server list -f json ' + '--changes-since ' + updated_at2 + )) + + col_updated = [server["Name"] for server in cmd_output] + self.assertNotIn(server_name1, col_updated) + self.assertIn(server_name2, col_updated) + self.assertIn(server_name3, col_updated) + + def test_server_list_with_changes_before_and_changes_since(self): + """Test server list. + + Getting the servers list with updated_at time equal or before than + changes-before and equal or later than changes-since. + """ + cmd_output = self.server_create() + server_name1 = cmd_output['name'] + cmd_output = self.server_create() + server_name2 = cmd_output['name'] + updated_at2 = cmd_output['updated'] + cmd_output = self.server_create() + server_name3 = cmd_output['name'] + updated_at3 = cmd_output['updated'] + + cmd_output = json.loads(self.openstack( + '--os-compute-api-version 2.66 ' + + 'server list -f json ' + + '--changes-since ' + updated_at2 + + ' --changes-before ' + updated_at3 + )) + + col_updated = [server["Name"] for server in cmd_output] + self.assertNotIn(server_name1, col_updated) + self.assertIn(server_name2, col_updated) + self.assertIn(server_name3, col_updated) + def test_server_set(self): """Test server create, delete, set, show""" cmd_output = self.server_create() diff --git a/openstackclient/tests/unit/api/test_utils.py b/openstackclient/tests/unit/api/test_utils.py deleted file mode 100644 index 1f528558..00000000 --- a/openstackclient/tests/unit/api/test_utils.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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. -# - -"""API Utilities Library Tests""" - -import copy - -from openstackclient.api import api -from openstackclient.api import utils as api_utils -from openstackclient.tests.unit.api import fakes as api_fakes - - -class TestBaseAPIFilter(api_fakes.TestSession): - """The filters can be tested independently""" - - def setUp(self): - super(TestBaseAPIFilter, self).setUp() - self.api = api.BaseAPI( - session=self.sess, - endpoint=self.BASE_URL, - ) - - self.input_list = [ - api_fakes.RESP_ITEM_1, - api_fakes.RESP_ITEM_2, - api_fakes.RESP_ITEM_3, - ] - - def test_simple_filter_none(self): - output = api_utils.simple_filter( - ) - self.assertIsNone(output) - - def test_simple_filter_no_attr(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - ) - self.assertEqual(self.input_list, output) - - def test_simple_filter_attr_only(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - ) - self.assertEqual(self.input_list, output) - - def test_simple_filter_attr_value(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - value='', - ) - self.assertEqual([], output) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - value='UP', - ) - self.assertEqual( - [api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_3], - output, - ) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='fred', - value='UP', - ) - self.assertEqual([], output) - - def test_simple_filter_prop_attr_only(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='b', - property_field='props', - ) - self.assertEqual(self.input_list, output) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='status', - property_field='props', - ) - self.assertEqual(self.input_list, output) - - def test_simple_filter_prop_attr_value(self): - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='b', - value=2, - property_field='props', - ) - self.assertEqual( - [api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_2], - output, - ) - - output = api_utils.simple_filter( - copy.deepcopy(self.input_list), - attr='b', - value=9, - property_field='props', - ) - self.assertEqual([], output) diff --git a/openstackclient/tests/unit/common/test_commandmanager.py b/openstackclient/tests/unit/common/test_commandmanager.py deleted file mode 100644 index 0c6c99c0..00000000 --- a/openstackclient/tests/unit/common/test_commandmanager.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2012-2013 OpenStack Foundation -# -# 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.common import commandmanager -from openstackclient.tests.unit import utils - - -class FakeCommand(object): - - @classmethod - def load(cls): - return cls - - def __init__(self): - return - -FAKE_CMD_ONE = FakeCommand -FAKE_CMD_TWO = FakeCommand -FAKE_CMD_ALPHA = FakeCommand -FAKE_CMD_BETA = FakeCommand - - -class FakeCommandManager(commandmanager.CommandManager): - commands = {} - - def load_commands(self, namespace): - if namespace == 'test': - self.commands['one'] = FAKE_CMD_ONE - self.commands['two'] = FAKE_CMD_TWO - self.group_list.append(namespace) - elif namespace == 'greek': - self.commands['alpha'] = FAKE_CMD_ALPHA - self.commands['beta'] = FAKE_CMD_BETA - self.group_list.append(namespace) - - -class TestCommandManager(utils.TestCase): - - def test_add_command_group(self): - mgr = FakeCommandManager('test') - - # Make sure add_command() still functions - mock_cmd_one = mock.Mock() - mgr.add_command('mock', mock_cmd_one) - cmd_mock, name, args = mgr.find_command(['mock']) - self.assertEqual(mock_cmd_one, cmd_mock) - - # Find a command added in initialization - cmd_one, name, args = mgr.find_command(['one']) - self.assertEqual(FAKE_CMD_ONE, cmd_one) - - # Load another command group - mgr.add_command_group('greek') - - # Find a new command - cmd_alpha, name, args = mgr.find_command(['alpha']) - self.assertEqual(FAKE_CMD_ALPHA, cmd_alpha) - - # Ensure that the original commands were not overwritten - cmd_two, name, args = mgr.find_command(['two']) - self.assertEqual(FAKE_CMD_TWO, cmd_two) - - def test_get_command_groups(self): - mgr = FakeCommandManager('test') - - # Make sure add_command() still functions - mock_cmd_one = mock.Mock() - mgr.add_command('mock', mock_cmd_one) - cmd_mock, name, args = mgr.find_command(['mock']) - self.assertEqual(mock_cmd_one, cmd_mock) - - # Load another command group - mgr.add_command_group('greek') - - gl = mgr.get_command_groups() - self.assertEqual(['test', 'greek'], gl) - - def test_get_command_names(self): - mock_cmd_one = mock.Mock() - mock_cmd_one.name = 'one' - mock_cmd_two = mock.Mock() - mock_cmd_two.name = 'cmd two' - mock_pkg_resources = mock.Mock( - return_value=[mock_cmd_one, mock_cmd_two], - ) - with mock.patch( - 'pkg_resources.iter_entry_points', - mock_pkg_resources, - ) as iter_entry_points: - mgr = commandmanager.CommandManager('test') - iter_entry_points.assert_called_once_with('test') - cmds = mgr.get_command_names('test') - self.assertEqual(['one', 'cmd two'], cmds) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c30af8fb..4a37a812 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -23,6 +23,7 @@ from openstack import exceptions as sdk_exceptions from osc_lib import exceptions from osc_lib import utils as common_utils from oslo_utils import timeutils +import six from openstackclient.compute.v2 import server from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -1834,6 +1835,90 @@ class TestServerCreate(TestServer): self.cmd.take_action, parsed_args) + def test_server_create_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.app.client_manager.compute.api_version = 2.19 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--description', 'description1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('description', 'description1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + # In base command class ShowOne in cliff, abstract method + # take_action() returns a two-part tuple with a tuple of + # column names and a tuple of data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping_v2=[], + nics='auto', + scheduler_hints={}, + config_drive=None, + description='description1', + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + self.assertFalse(self.images_mock.called) + self.assertFalse(self.flavors_mock.called) + + def test_server_create_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.app.client_manager.compute.api_version = 2.18 + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--description', 'description1', + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('description', 'description1'), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerDelete(TestServer): @@ -1991,6 +2076,7 @@ class TestServerList(TestServer): 'user_id': None, 'deleted': False, 'changes-since': None, + 'changes-before': None, } # Default params of the core function of the command in the case of no @@ -2272,6 +2358,71 @@ class TestServerList(TestServer): 'Invalid time value' ) + def test_server_list_v266_with_changes_before(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.66')) + arglist = [ + '--changes-before', '2016-03-05T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_before', '2016-03-05T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.search_opts['changes-before'] = '2016-03-05T06:27:59Z' + self.search_opts['deleted'] = True + self.servers_mock.list.assert_called_with(**self.kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError) + def test_server_list_v266_with_invalid_changes_before( + self, mock_parse_isotime): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.66')) + + arglist = [ + '--changes-before', 'Invalid time value', + ] + verifylist = [ + ('changes_before', 'Invalid time value'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('Invalid changes-before value: Invalid time ' + 'value', str(e)) + mock_parse_isotime.assert_called_once_with( + 'Invalid time value' + ) + + def test_server_with_changes_before_older_version(self): + self.app.client_manager.compute.api_version = ( + api_versions.APIVersion('2.65')) + + arglist = [ + '--changes-before', '2016-03-05T06:27:59Z', + '--deleted' + ] + verifylist = [ + ('changes_before', '2016-03-05T06:27:59Z'), + ('deleted', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + def test_server_list_v269_with_partial_constructs(self): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.69') @@ -2415,12 +2566,40 @@ class TestServerMigrate(TestServer): self.assertNotCalled(self.servers_mock.live_migrate) self.assertNotCalled(self.servers_mock.migrate) + def test_server_migrate_with_host(self): + # Tests that --host is not allowed for a cold migration. + arglist = [ + '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', False), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # Make sure it's the error we expect. + self.assertIn("--live-migration must be specified if " + "--block-migration, --disk-overcommit or --host is " + "specified", six.text_type(ex)) + self.servers_mock.get.assert_called_with(self.server.id) + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertNotCalled(self.servers_mock.migrate) + def test_server_live_migrate(self): arglist = [ '--live', 'fakehost', self.server.id, ] verifylist = [ ('live', 'fakehost'), + ('live_migration', False), + ('host', None), ('block_migration', False), ('disk_overcommit', False), ('wait', False), @@ -2430,7 +2609,8 @@ class TestServerMigrate(TestServer): self.app.client_manager.compute.api_version = \ api_versions.APIVersion('2.24') - result = self.cmd.take_action(parsed_args) + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with(self.server.id) self.server.live_migrate.assert_called_with(block_migration=False, @@ -2438,6 +2618,132 @@ class TestServerMigrate(TestServer): host='fakehost') self.assertNotCalled(self.servers_mock.migrate) self.assertIsNone(result) + # A warning should have been logged for using --live. + mock_warning.assert_called_once() + self.assertIn('The --live option has been deprecated.', + six.text_type(mock_warning.call_args[0][0])) + + def test_server_live_migrate_host_pre_2_30(self): + # Tests that the --host option is not supported for --live-migration + # before microversion 2.30 (the test defaults to 2.1). + arglist = [ + '--live-migration', '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # Make sure it's the error we expect. + self.assertIn('--os-compute-api-version 2.30 or greater is required ' + 'when using --host', six.text_type(ex)) + + self.servers_mock.get.assert_called_with(self.server.id) + self.assertNotCalled(self.servers_mock.live_migrate) + self.assertNotCalled(self.servers_mock.migrate) + + def test_server_live_migrate_no_host(self): + # Tests the --live-migration option without --host or --live. + arglist = [ + '--live-migration', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('host', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=False, + host=None) + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + # Since --live wasn't used a warning shouldn't have been logged. + mock_warning.assert_not_called() + + def test_server_live_migrate_with_host(self): + # Tests the --live-migration option with --host but no --live. + # This requires --os-compute-api-version >= 2.30 so the test uses 2.30. + arglist = [ + '--live-migration', '--host', 'fakehost', self.server.id, + ] + verifylist = [ + ('live', None), + ('live_migration', True), + ('host', 'fakehost'), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.30') + + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + # No disk_overcommit with microversion >= 2.25. + self.server.live_migrate.assert_called_with(block_migration=False, + host='fakehost') + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + + def test_server_live_migrate_without_host_override_live(self): + # Tests the --live-migration option without --host and with --live. + # The --live-migration option will take precedence and a warning is + # logged for using --live. + arglist = [ + '--live', 'fakehost', '--live-migration', self.server.id, + ] + verifylist = [ + ('live', 'fakehost'), + ('live_migration', True), + ('host', None), + ('block_migration', False), + ('disk_overcommit', False), + ('wait', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(self.cmd.log, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.live_migrate.assert_called_with(block_migration=False, + disk_over_commit=False, + host=None) + self.assertNotCalled(self.servers_mock.migrate) + self.assertIsNone(result) + # A warning should have been logged for using --live. + mock_warning.assert_called_once() + self.assertIn('The --live option has been deprecated.', + six.text_type(mock_warning.call_args[0][0])) + + def test_server_live_migrate_live_and_host_mutex(self): + # Tests specifying both the --live and --host options which are in a + # mutex group so argparse should fail. + arglist = [ + '--live', 'fakehost', '--host', 'fakehost', self.server.id, + ] + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, verify_args=[]) def test_server_block_live_migrate(self): arglist = [ @@ -2663,6 +2969,55 @@ class TestServerRebuild(TestServer): self.images_mock.get.assert_called_with(self.image.id) self.server.rebuild.assert_called_with(self.image, password) + def test_rebuild_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.server.api_version = 2.18 + + description = 'description1' + arglist = [ + self.server.id, + '--description', description + ] + verifylist = [ + ('server', self.server.id), + ('description', description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_rebuild_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.server.api_version = 2.19 + + description = 'description1' + arglist = [ + self.server.id, + '--description', description + ] + verifylist = [ + ('server', self.server.id), + ('description', description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.images_mock.get.assert_called_with(self.image.id) + self.server.rebuild.assert_called_with(self.image, None, + description=description) + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_rebuild_with_wait_ok(self, mock_wait_for_status): arglist = [ @@ -3400,6 +3755,10 @@ class TestServerSet(TestServer): def setUp(self): super(TestServerSet, self).setUp() + self.attrs = { + 'api_version': None, + } + self.methods = { 'update': None, 'reset_state': None, @@ -3502,6 +3861,48 @@ class TestServerSet(TestServer): mock.sentinel.fake_pass) self.assertIsNone(result) + def test_server_set_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.fake_servers[0].api_version = 2.19 + + arglist = [ + '--description', 'foo_description', + 'foo_vm', + ] + verifylist = [ + ('description', 'foo_description'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + result = self.cmd.take_action(parsed_args) + self.fake_servers[0].update.assert_called_once_with( + description='foo_description') + self.assertIsNone(result) + + def test_server_set_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.fake_servers[0].api_version = 2.18 + + arglist = [ + '--description', 'foo_description', + 'foo_vm', + ] + verifylist = [ + ('description', 'foo_description'), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerShelve(TestServer): @@ -3783,6 +4184,50 @@ class TestServerUnset(TestServer): self.fake_server, ['key1', 'key2']) self.assertIsNone(result) + def test_server_unset_with_description_api_newer(self): + + # Description is supported for nova api version 2.19 or above + self.app.client_manager.compute.api_version = 2.19 + + arglist = [ + '--description', + 'foo_vm', + ] + verifylist = [ + ('description', True), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + result = self.cmd.take_action(parsed_args) + self.servers_mock.update.assert_called_once_with( + self.fake_server, description="") + self.assertIsNone(result) + + def test_server_unset_with_description_api_older(self): + + # Description is not supported for nova api version below 2.19 + self.app.client_manager.compute.api_version = 2.18 + + arglist = [ + '--description', + 'foo_vm', + ] + verifylist = [ + ('description', True), + ('server', 'foo_vm'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch.object(api_versions, + 'APIVersion', + return_value=2.19): + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + class TestServerUnshelve(TestServer): diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index 27ee9fd0..e5727a6a 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -235,6 +235,10 @@ endpoint_group_filters = { 'service_id': service_id, 'region_id': endpoint_region, } +endpoint_group_filters_2 = { + 'region_id': endpoint_region, +} +endpoint_group_file_path = '/tmp/path/to/file' ENDPOINT_GROUP = { 'id': endpoint_group_id, @@ -1044,6 +1048,64 @@ class FakeEndpoint(object): return endpoint_filter +class FakeEndpointGroup(object): + """Fake one or more endpoint group.""" + + @staticmethod + def create_one_endpointgroup(attrs=None): + """Create a fake endpoint group. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id, url, and so on + """ + + attrs = attrs or {} + + # set default attributes. + endpointgroup_info = { + 'id': 'endpoint-group-id-' + uuid.uuid4().hex, + 'name': 'endpoint-group-name-' + uuid.uuid4().hex, + 'filters': { + 'region': 'region-' + uuid.uuid4().hex, + 'service_id': 'service-id-' + uuid.uuid4().hex, + }, + 'description': 'endpoint-group-description-' + uuid.uuid4().hex, + 'links': 'links-' + uuid.uuid4().hex, + } + endpointgroup_info.update(attrs) + + endpoint = fakes.FakeResource(info=copy.deepcopy(endpointgroup_info), + loaded=True) + return endpoint + + @staticmethod + def create_one_endpointgroup_filter(attrs=None): + """Create a fake endpoint project relationship. + + :param Dictionary attrs: + A dictionary with all attributes of endpointgroup filter + :return: + A FakeResource object with project, endpointgroup and so on + """ + attrs = attrs or {} + + # Set default attribute + endpointgroup_filter_info = { + 'project': 'project-id-' + uuid.uuid4().hex, + 'endpointgroup': 'endpointgroup-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes if there are some attributes set + endpointgroup_filter_info.update(attrs) + + endpointgroup_filter = fakes.FakeModel( + copy.deepcopy(endpointgroup_filter_info)) + + return endpointgroup_filter + + class FakeService(object): """Fake one or more service.""" diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint_group.py b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py new file mode 100644 index 00000000..6e9da9c7 --- /dev/null +++ b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py @@ -0,0 +1,495 @@ +# 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 endpoint_group +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +class TestEndpointGroup(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestEndpointGroup, self).setUp() + + # Get a shortcut to the EndpointManager Mock + self.endpoint_groups_mock = ( + self.app.client_manager.identity.endpoint_groups + ) + self.endpoint_groups_mock.reset_mock() + self.epf_mock = ( + self.app.client_manager.identity.endpoint_filter + ) + self.epf_mock.reset_mock() + + # Get a shortcut to the ServiceManager Mock + self.services_mock = self.app.client_manager.identity.services + self.services_mock.reset_mock() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + +class TestEndpointGroupCreate(TestEndpointGroup): + + columns = ( + 'description', + 'filters', + 'id', + 'name', + ) + + def setUp(self): + super(TestEndpointGroupCreate, self).setUp() + + self.endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup( + attrs={'filters': identity_fakes.endpoint_group_filters})) + + self.endpoint_groups_mock.create.return_value = self.endpoint_group + + # Get the command object to test + self.cmd = endpoint_group.CreateEndpointGroup(self.app, None) + + def test_endpointgroup_create_no_options(self): + arglist = [ + '--description', self.endpoint_group.description, + self.endpoint_group.name, + identity_fakes.endpoint_group_file_path, + ] + verifylist = [ + ('name', self.endpoint_group.name), + ('filters', identity_fakes.endpoint_group_file_path), + ('description', self.endpoint_group.description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.endpoint_group_filters + with mock.patch("openstackclient.identity.v3.endpoint_group." + "CreateEndpointGroup._read_filters", mocker): + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': self.endpoint_group.name, + 'filters': identity_fakes.endpoint_group_filters, + 'description': self.endpoint_group.description, + } + + self.endpoint_groups_mock.create.assert_called_with( + **kwargs + ) + + self.assertEqual(self.columns, columns) + datalist = ( + self.endpoint_group.description, + identity_fakes.endpoint_group_filters, + self.endpoint_group.id, + self.endpoint_group.name, + ) + self.assertEqual(datalist, data) + + +class TestEndpointGroupDelete(TestEndpointGroup): + + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + def setUp(self): + super(TestEndpointGroupDelete, self).setUp() + + # This is the return value for utils.find_resource(endpoint) + self.endpoint_groups_mock.get.return_value = self.endpoint_group + self.endpoint_groups_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint_group.DeleteEndpointGroup(self.app, None) + + def test_endpointgroup_delete(self): + arglist = [ + self.endpoint_group.id, + ] + verifylist = [ + ('endpointgroup', [self.endpoint_group.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.endpoint_groups_mock.delete.assert_called_with( + self.endpoint_group.id, + ) + self.assertIsNone(result) + + +class TestEndpointGroupList(TestEndpointGroup): + + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + + columns = ( + 'ID', + 'Name', + 'Description', + ) + + def setUp(self): + super(TestEndpointGroupList, self).setUp() + + self.endpoint_groups_mock.list.return_value = [self.endpoint_group] + self.endpoint_groups_mock.get.return_value = self.endpoint_group + self.epf_mock.list_projects_for_endpoint_group.return_value = [ + self.project] + self.epf_mock.list_endpoint_groups_for_project.return_value = [ + self.endpoint_group] + + # Get the command object to test + self.cmd = endpoint_group.ListEndpointGroup(self.app, None) + + def test_endpoint_group_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + self.endpoint_groups_mock.list.assert_called_with() + + self.assertEqual(self.columns, columns) + datalist = ( + ( + self.endpoint_group.id, + self.endpoint_group.name, + self.endpoint_group.description, + ), + ) + self.assertEqual(datalist, tuple(data)) + + def test_endpoint_group_list_projects_by_endpoint_group(self): + arglist = [ + '--endpointgroup', self.endpoint_group.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + self.epf_mock.list_projects_for_endpoint_group.assert_called_with( + endpoint_group=self.endpoint_group.id + ) + + self.assertEqual(self.columns, columns) + datalist = ( + ( + self.project.id, + self.project.name, + self.project.description, + ), + ) + self.assertEqual(datalist, tuple(data)) + + def test_endpoint_group_list_by_project(self): + self.epf_mock.list_endpoints_for_project.return_value = [ + self.endpoint_group + ] + self.projects_mock.get.return_value = self.project + + arglist = [ + '--project', self.project.name, + '--domain', self.domain.name + ] + verifylist = [ + ('project', self.project.name), + ('domain', self.domain.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + self.epf_mock.list_endpoint_groups_for_project.assert_called_with( + project=self.project.id + ) + + self.assertEqual(self.columns, columns) + datalist = ( + ( + self.endpoint_group.id, + self.endpoint_group.name, + self.endpoint_group.description, + ), + ) + self.assertEqual(datalist, tuple(data)) + + +class TestEndpointGroupSet(TestEndpointGroup): + + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + def setUp(self): + super(TestEndpointGroupSet, self).setUp() + + # This is the return value for utils.find_resource(endpoint) + self.endpoint_groups_mock.get.return_value = self.endpoint_group + + self.endpoint_groups_mock.update.return_value = self.endpoint_group + + # Get the command object to test + self.cmd = endpoint_group.SetEndpointGroup(self.app, None) + + def test_endpoint_group_set_no_options(self): + arglist = [ + self.endpoint_group.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + kwargs = { + 'name': None, + 'filters': None, + 'description': '' + } + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_endpoint_group_set_name(self): + arglist = [ + '--name', 'qwerty', + self.endpoint_group.id + ] + verifylist = [ + ('name', 'qwerty'), + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'qwerty', + 'filters': None, + 'description': '' + } + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + self.assertIsNone(result) + + def test_endpoint_group_set_filters(self): + arglist = [ + '--filters', identity_fakes.endpoint_group_file_path, + self.endpoint_group.id, + ] + verifylist = [ + ('filters', identity_fakes.endpoint_group_file_path), + ('endpointgroup', self.endpoint_group.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.endpoint_group_filters_2 + with mock.patch("openstackclient.identity.v3.endpoint_group." + "SetEndpointGroup._read_filters", mocker): + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': None, + 'filters': identity_fakes.endpoint_group_filters_2, + 'description': '', + } + + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + + self.assertIsNone(result) + + def test_endpoint_group_set_description(self): + arglist = [ + '--description', 'qwerty', + self.endpoint_group.id + ] + verifylist = [ + ('description', 'qwerty'), + ('endpointgroup', self.endpoint_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': None, + 'filters': None, + 'description': 'qwerty', + } + self.endpoint_groups_mock.update.assert_called_with( + self.endpoint_group.id, + **kwargs + ) + self.assertIsNone(result) + + +class TestAddProjectToEndpointGroup(TestEndpointGroup): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + new_ep_filter = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup_filter( + attrs={'endpointgroup': endpoint_group.id, + 'project': project.id})) + + def setUp(self): + super(TestAddProjectToEndpointGroup, self).setUp() + + # This is the return value for utils.find_resource() + self.endpoint_groups_mock.get.return_value = self.endpoint_group + + # Update the image_id in the MEMBER dict + self.epf_mock.create.return_value = self.new_ep_filter + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + + # Get the command object to test + self.cmd = endpoint_group.AddProjectToEndpointGroup(self.app, None) + + def test_add_project_to_endpoint_no_option(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.epf_mock.add_endpoint_group_to_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) + + def test_add_project_to_endpoint_with_option(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + '--project-domain', self.domain.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ('project_domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.epf_mock.add_endpoint_group_to_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) + + +class TestRemoveProjectEndpointGroup(TestEndpointGroup): + + project = identity_fakes.FakeProject.create_one_project() + domain = identity_fakes.FakeDomain.create_one_domain() + endpoint_group = ( + identity_fakes.FakeEndpointGroup.create_one_endpointgroup()) + + def setUp(self): + super(TestRemoveProjectEndpointGroup, self).setUp() + + # This is the return value for utils.find_resource() + self.endpoint_groups_mock.get.return_value = self.endpoint_group + + self.projects_mock.get.return_value = self.project + self.domains_mock.get.return_value = self.domain + self.epf_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint_group.RemoveProjectFromEndpointGroup( + self.app, None) + + def test_remove_project_endpoint_no_options(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.epf_mock.delete_endpoint_group_from_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) + + def test_remove_project_endpoint_with_options(self): + arglist = [ + self.endpoint_group.id, + self.project.id, + '--project-domain', self.domain.id, + ] + verifylist = [ + ('endpointgroup', self.endpoint_group.id), + ('project', self.project.id), + ('project_domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.epf_mock.delete_endpoint_group_from_project.assert_called_with( + project=self.project.id, + endpoint_group=self.endpoint_group.id, + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index ae578d91..6babd4ff 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -417,7 +417,7 @@ class TestImageList(TestImage): ), ) self.assertEqual(datalist, tuple(data)) - @mock.patch('openstackclient.api.utils.simple_filter') + @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): sf_mock.side_effect = [ [self.image_info], [], diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 16a393df..4cc8f448 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -734,7 +734,7 @@ class TestImageList(TestImage): ), ) self.assertEqual(datalist, tuple(data)) - @mock.patch('openstackclient.api.utils.simple_filter') + @mock.patch('osc_lib.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): sf_mock.return_value = [copy.deepcopy(self._image)] |
