summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml3
-rw-r--r--Dockerfile3
-rw-r--r--babel.cfg1
-rw-r--r--bindep.txt1
-rw-r--r--lower-constraints.txt9
-rw-r--r--openstackclient/common/clientmanager.py26
-rw-r--r--openstackclient/common/quota.py21
-rw-r--r--openstackclient/compute/v2/flavor.py2
-rw-r--r--openstackclient/compute/v2/server.py33
-rw-r--r--openstackclient/compute/v2/server_group.py8
-rw-r--r--openstackclient/identity/common.py2
-rw-r--r--openstackclient/identity/v3/role.py68
-rw-r--r--openstackclient/image/v2/image.py17
-rw-r--r--openstackclient/network/v2/port.py54
-rw-r--r--openstackclient/tests/functional/base.py4
-rw-r--r--openstackclient/tests/functional/common/test_quota.py44
-rw-r--r--openstackclient/tests/functional/compute/v2/test_server.py58
-rw-r--r--openstackclient/tests/functional/examples/__init__.py0
-rw-r--r--openstackclient/tests/functional/examples/test_examples.py28
-rw-r--r--openstackclient/tests/unit/common/test_quota.py119
-rw-r--r--openstackclient/tests/unit/compute/v2/fakes.py35
-rw-r--r--openstackclient/tests/unit/compute/v2/test_flavor.py36
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py11
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server_group.py97
-rw-r--r--openstackclient/tests/unit/identity/v3/test_role.py379
-rw-r--r--openstackclient/tests/unit/image/v2/test_image.py22
-rw-r--r--openstackclient/tests/unit/network/v2/fakes.py1
-rw-r--r--openstackclient/tests/unit/network/v2/test_port.py169
-rw-r--r--releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml5
-rw-r--r--releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml5
-rw-r--r--releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml7
-rw-r--r--releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml6
-rw-r--r--releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml7
-rw-r--r--releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml6
-rw-r--r--releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml4
-rw-r--r--releasenotes/notes/task-40279-eb0d718ac1959c50.yaml5
-rw-r--r--requirements.txt3
-rw-r--r--setup.cfg17
-rw-r--r--test-requirements.txt1
-rw-r--r--tox.ini1
40 files changed, 1169 insertions, 149 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index 734b1ede..c04f3446 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -51,8 +51,6 @@
vars:
devstack_localrc:
LIBS_FROM_GIT: python-openstackclient
- # NOTE(dtroyer): OSC needs to support Image v1 for a while yet so re-enable
- GLANCE_V1_ENABLED: true
# NOTE(dtroyer): Functional tests need a bit more volume headroom
VOLUME_BACKING_FILE_SIZE: 20G
devstack_local_conf:
@@ -93,6 +91,7 @@
neutron-segments: true
q-metering: true
q-qos: true
+ neutron-tag-ports-during-bulk-creation: true
tox_envlist: functional
- job:
diff --git a/Dockerfile b/Dockerfile
index ca60bb0e..bf5de3c7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,4 +23,7 @@ FROM docker.io/opendevorg/python-base:3.7
COPY --from=builder /output/ /output
RUN /output/install-from-bindep
+# Trigger entrypoint loading to trigger stevedore entrypoint caching
+RUN openstack --help >/dev/null 2>&1
+
CMD ["/usr/local/bin/openstack"]
diff --git a/babel.cfg b/babel.cfg
deleted file mode 100644
index efceab81..00000000
--- a/babel.cfg
+++ /dev/null
@@ -1 +0,0 @@
-[python: **.py]
diff --git a/bindep.txt b/bindep.txt
index cf94d2a8..4c90a026 100644
--- a/bindep.txt
+++ b/bindep.txt
@@ -5,7 +5,6 @@ gcc [compile test]
libc6-dev [compile test platform:dpkg]
libffi-devel [platform:rpm]
libffi-dev [compile test platform:dpkg]
-libffi6 [platform:dpkg]
libssl-dev [compile test platform:dpkg]
python3-dev [compile test platform:dpkg]
python3-devel [compile test platform:rpm]
diff --git a/lower-constraints.txt b/lower-constraints.txt
index f16eca41..403ba4e0 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -2,10 +2,9 @@ amqp==2.1.1
aodhclient==0.9.0
appdirs==1.3.0
asn1crypto==0.23.0
-Babel==2.3.4
bandit==1.1.0
cachetools==2.0.0
-cffi==1.7.0
+cffi==1.14.0
cliff==2.8.0
cmd2==0.8.0
contextlib2==0.4.0
@@ -29,7 +28,7 @@ futurist==2.1.0
gitdb==0.6.4
GitPython==1.0.1
gnocchiclient==3.3.1
-greenlet==0.4.10
+greenlet==0.4.15
hacking==2.0.0
httplib2==0.9.1
idna==2.6
@@ -50,7 +49,7 @@ msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
-openstacksdk==0.44.0
+openstacksdk==0.48.0
os-service-types==1.7.0
os-testr==1.0.0
osc-lib==2.0.0
@@ -124,7 +123,7 @@ six==1.10.0
smmap==0.9.0
statsd==3.2.1
stestr==1.0.0
-stevedore==1.20.0
+stevedore==2.0.1
sushy==0.1.0
tempest==17.1.0
tenacity==3.2.1
diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index c1118ad3..36c3ce26 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -15,12 +15,13 @@
"""Manage access to the clients, including authenticating when needed."""
+import importlib
import logging
import sys
from osc_lib import clientmanager
from osc_lib import shell
-import pkg_resources
+import stevedore
LOG = logging.getLogger(__name__)
@@ -143,17 +144,28 @@ class ClientManager(clientmanager.ClientManager):
def get_plugin_modules(group):
"""Find plugin entry points"""
mod_list = []
- for ep in pkg_resources.iter_entry_points(group):
+ mgr = stevedore.ExtensionManager(group)
+ for ep in mgr:
LOG.debug('Found plugin %s', ep.name)
+ # Different versions of stevedore use different
+ # implementations of EntryPoint from other libraries, which
+ # are not API-compatible.
try:
- __import__(ep.module_name)
- except Exception:
+ module_name = ep.entry_point.module_name
+ except AttributeError:
+ try:
+ module_name = ep.entry_point.module
+ except AttributeError:
+ module_name = ep.entry_point.value
+
+ try:
+ module = importlib.import_module(module_name)
+ except Exception as err:
sys.stderr.write(
- "WARNING: Failed to import plugin %s.\n" % ep.name)
+ "WARNING: Failed to import plugin %s: %s.\n" % (ep.name, err))
continue
- module = sys.modules[ep.module_name]
mod_list.append(module)
init_func = getattr(module, 'Initialize', None)
if init_func:
@@ -164,7 +176,7 @@ def get_plugin_modules(group):
clientmanager.ClientManager,
module.API_NAME,
clientmanager.ClientCache(
- getattr(sys.modules[ep.module_name], 'make_client', None)
+ getattr(sys.modules[module_name], 'make_client', None)
),
)
return mod_list
diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py
index 37437344..11de986b 100644
--- a/openstackclient/common/quota.py
+++ b/openstackclient/common/quota.py
@@ -274,9 +274,18 @@ class ListQuota(command.Lister, BaseQuota):
return parser
def take_action(self, parsed_args):
- projects = self.app.client_manager.identity.projects.list()
result = []
- project_ids = [getattr(p, 'id', '') for p in projects]
+ project_ids = []
+ if parsed_args.project is None:
+ for p in self.app.client_manager.identity.projects.list():
+ project_ids.append(getattr(p, 'id', ''))
+ else:
+ identity_client = self.app.client_manager.identity
+ project = utils.find_resource(
+ identity_client.projects,
+ parsed_args.project,
+ )
+ project_ids.append(getattr(project, 'id', ''))
if parsed_args.compute:
if parsed_args.detail:
@@ -516,6 +525,11 @@ class SetQuota(common.NetDetectionMixin, command.Command):
metavar='<volume-type>',
help=_('Set quotas for a specific <volume-type>'),
)
+ parser.add_argument(
+ '--force',
+ action='store_true',
+ help=_('Force quota update (only supported by compute)')
+ )
return parser
def take_action(self, parsed_args):
@@ -529,6 +543,9 @@ class SetQuota(common.NetDetectionMixin, command.Command):
if value is not None:
compute_kwargs[k] = value
+ if parsed_args.force:
+ compute_kwargs['force'] = True
+
volume_kwargs = {}
for k, v in VOLUME_QUOTAS.items():
value = getattr(parsed_args, k, None)
diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py
index 42649db5..805e919e 100644
--- a/openstackclient/compute/v2/flavor.py
+++ b/openstackclient/compute/v2/flavor.py
@@ -402,7 +402,7 @@ class SetFlavor(command.Command):
if compute_client.api_version < api_versions.APIVersion("2.55"):
msg = _("--os-compute-api-version 2.55 or later is required")
raise exceptions.CommandError(msg)
- compute_client.flavors.update(flavor=parsed_args.flavor,
+ compute_client.flavors.update(flavor=flavor.id,
description=parsed_args.description)
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 93e9f966..69756ae2 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -24,6 +24,7 @@ import os
from novaclient import api_versions
from novaclient.v2 import servers
from openstack import exceptions as sdk_exceptions
+from osc_lib.cli import format_columns
from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
@@ -38,6 +39,8 @@ from openstackclient.network import common as network_common
LOG = logging.getLogger(__name__)
+IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)'
+
def _format_servers_list_networks(networks):
"""Return a formatted string of a server's networks
@@ -147,6 +150,12 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
info['image'] = "%s (%s)" % (image.name, image_id)
except Exception:
info['image'] = image_id
+ else:
+ # NOTE(melwitt): An server booted from a volume will have no image
+ # associated with it. We fill in the image with "N/A (booted from
+ # volume)" to help users who want to be able to grep for
+ # boot-from-volume servers when using the CLI.
+ info['image'] = IMAGE_STRING_FOR_BFV
# Convert the flavor blob to a name
flavor_info = info.get('flavor', {})
@@ -166,14 +175,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
if 'os-extended-volumes:volumes_attached' in info:
info.update(
{
- 'volumes_attached': utils.format_list_of_dicts(
+ 'volumes_attached': format_columns.ListDictColumn(
info.pop('os-extended-volumes:volumes_attached'))
}
)
if 'security_groups' in info:
info.update(
{
- 'security_groups': utils.format_list_of_dicts(
+ 'security_groups': format_columns.ListDictColumn(
info.pop('security_groups'))
}
)
@@ -182,9 +191,14 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
info['addresses'] = _format_servers_list_networks(server.networks)
# Map 'metadata' field to 'properties'
- info.update(
- {'properties': utils.format_dict(info.pop('metadata'))}
- )
+ if not info['metadata']:
+ info.update(
+ {'properties': utils.format_dict(info.pop('metadata'))}
+ )
+ else:
+ info.update(
+ {'properties': format_columns.DictColumn(info.pop('metadata'))}
+ )
# Migrate tenant_id to project_id naming
if 'tenant_id' in info:
@@ -1520,8 +1534,12 @@ class ListServer(command.Lister):
s.image_name = image.name
s.image_id = s.image['id']
else:
- s.image_name = ''
- s.image_id = ''
+ # NOTE(melwitt): An server booted from a volume will have no
+ # image associated with it. We fill in the Image Name and ID
+ # with "N/A (booted from volume)" to help users who want to be
+ # able to grep for boot-from-volume servers when using the CLI.
+ s.image_name = IMAGE_STRING_FOR_BFV
+ s.image_id = IMAGE_STRING_FOR_BFV
if 'id' in s.flavor:
flavor = flavors.get(s.flavor['id'])
if flavor:
@@ -2530,7 +2548,6 @@ class ShowServer(command.ShowOne):
data = _prep_server_detail(compute_client,
self.app.client_manager.image, server,
refresh=False)
-
return zip(*sorted(data.items()))
diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py
index 9bc2bb43..1af6e28d 100644
--- a/openstackclient/compute/v2/server_group.py
+++ b/openstackclient/compute/v2/server_group.py
@@ -141,11 +141,15 @@ class ListServerGroup(command.Lister):
compute_client = self.app.client_manager.compute
data = compute_client.server_groups.list(parsed_args.all_projects)
+ policy_key = 'Policies'
+ if compute_client.api_version >= api_versions.APIVersion("2.64"):
+ policy_key = 'Policy'
+
if parsed_args.long:
column_headers = columns = (
'ID',
'Name',
- 'Policies',
+ policy_key,
'Members',
'Project Id',
'User Id',
@@ -154,7 +158,7 @@ class ListServerGroup(command.Lister):
column_headers = columns = (
'ID',
'Name',
- 'Policies',
+ policy_key,
)
return (column_headers,
diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py
index e70d87d2..a75db4f8 100644
--- a/openstackclient/identity/common.py
+++ b/openstackclient/identity/common.py
@@ -207,7 +207,7 @@ def _find_identity_resource(identity_client_manager, name_or_id,
name_or_id, **kwargs)
if identity_resource is not None:
return identity_resource
- except exceptions.Forbidden:
+ except (exceptions.Forbidden, identity_exc.Forbidden):
pass
return resource_type(None, {'id': name_or_id, 'name': name_or_id})
diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py
index 980ebf11..a674564f 100644
--- a/openstackclient/identity/v3/role.py
+++ b/openstackclient/identity/v3/role.py
@@ -64,59 +64,61 @@ def _add_identity_and_resource_options_to_parser(parser):
def _process_identity_and_resource_options(parsed_args,
- identity_client_manager):
+ identity_client_manager,
+ validate_actor_existence=True):
+
+ def _find_user():
+ try:
+ return common.find_user(
+ identity_client_manager,
+ parsed_args.user,
+ parsed_args.user_domain
+ ).id
+ except exceptions.CommandError:
+ if not validate_actor_existence:
+ return parsed_args.user
+ raise
+
+ def _find_group():
+ try:
+ return common.find_group(
+ identity_client_manager,
+ parsed_args.group,
+ parsed_args.group_domain
+ ).id
+ except exceptions.CommandError:
+ if not validate_actor_existence:
+ return parsed_args.group
+ raise
+
kwargs = {}
if parsed_args.user and parsed_args.system:
- kwargs['user'] = common.find_user(
- identity_client_manager,
- parsed_args.user,
- parsed_args.user_domain,
- ).id
+ kwargs['user'] = _find_user()
kwargs['system'] = parsed_args.system
elif parsed_args.user and parsed_args.domain:
- kwargs['user'] = common.find_user(
- identity_client_manager,
- parsed_args.user,
- parsed_args.user_domain,
- ).id
+ kwargs['user'] = _find_user()
kwargs['domain'] = common.find_domain(
identity_client_manager,
parsed_args.domain,
).id
elif parsed_args.user and parsed_args.project:
- kwargs['user'] = common.find_user(
- identity_client_manager,
- parsed_args.user,
- parsed_args.user_domain,
- ).id
+ kwargs['user'] = _find_user()
kwargs['project'] = common.find_project(
identity_client_manager,
parsed_args.project,
parsed_args.project_domain,
).id
elif parsed_args.group and parsed_args.system:
- kwargs['group'] = common.find_group(
- identity_client_manager,
- parsed_args.group,
- parsed_args.group_domain,
- ).id
+ kwargs['group'] = _find_group()
kwargs['system'] = parsed_args.system
elif parsed_args.group and parsed_args.domain:
- kwargs['group'] = common.find_group(
- identity_client_manager,
- parsed_args.group,
- parsed_args.group_domain,
- ).id
+ kwargs['group'] = _find_group()
kwargs['domain'] = common.find_domain(
identity_client_manager,
parsed_args.domain,
).id
elif parsed_args.group and parsed_args.project:
- kwargs['group'] = common.find_group(
- identity_client_manager,
- parsed_args.group,
- parsed_args.group_domain,
- ).id
+ kwargs['group'] = _find_group()
kwargs['project'] = common.find_project(
identity_client_manager,
parsed_args.project,
@@ -340,7 +342,9 @@ class RemoveRole(command.Command):
)
kwargs = _process_identity_and_resource_options(
- parsed_args, self.app.client_manager.identity)
+ parsed_args, self.app.client_manager.identity,
+ validate_actor_existence=False
+ )
identity_client.roles.revoke(role.id, **kwargs)
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py
index 53ce560d..029f57a3 100644
--- a/openstackclient/image/v2/image.py
+++ b/openstackclient/image/v2/image.py
@@ -324,6 +324,14 @@ class CreateImage(command.ShowOne):
metavar="<project>",
help=_("Set an alternate project on this image (name or ID)"),
)
+ parser.add_argument(
+ "--import",
+ dest="use_import",
+ action="store_true",
+ help=_(
+ "Force the use of glance image import instead of"
+ " direct upload")
+ )
common.add_project_domain_option_to_parser(parser)
for deadopt in self.deadopts:
parser.add_argument(
@@ -388,6 +396,9 @@ class CreateImage(command.ShowOne):
parsed_args.project_domain,
).id
+ if parsed_args.use_import:
+ kwargs['use_import'] = True
+
# open the file first to ensure any failures are handled before the
# image is created. Get the file name (if it is file, and not stdin)
# for easier further handling.
@@ -429,8 +440,14 @@ class CreateImage(command.ShowOne):
prompt=("Please enter private key password, leave "
"empty if none: "),
confirm=False)
+
if not pw or len(pw) < 1:
pw = None
+ else:
+ # load_private_key() requires the password to be
+ # passed as bytes
+ pw = pw.encode()
+
signer.load_private_key(
sign_key_path,
password=pw)
diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py
index 68154c3d..02ab06c1 100644
--- a/openstackclient/network/v2/port.py
+++ b/openstackclient/network/v2/port.py
@@ -158,6 +158,16 @@ def _get_attrs(client_manager, parsed_args):
parsed_args.disable_uplink_status_propagation):
attrs['propagate_uplink_status'] = False
+ if ('numa_policy_required' in parsed_args and
+ parsed_args.numa_policy_required):
+ attrs['numa_affinity_policy'] = 'required'
+ elif ('numa_policy_preferred' in parsed_args and
+ parsed_args.numa_policy_preferred):
+ attrs['numa_affinity_policy'] = 'preferred'
+ elif ('numa_policy_legacy' in parsed_args and
+ parsed_args.numa_policy_legacy):
+ attrs['numa_affinity_policy'] = 'legacy'
+
return attrs
@@ -265,6 +275,22 @@ def _add_updatable_args(parser):
help=_("Set DNS name for this port "
"(requires DNS integration extension)")
)
+ numa_affinity_policy_group = parser.add_mutually_exclusive_group()
+ numa_affinity_policy_group.add_argument(
+ '--numa-policy-required',
+ action='store_true',
+ help=_("NUMA affinity policy required to schedule this port")
+ )
+ numa_affinity_policy_group.add_argument(
+ '--numa-policy-preferred',
+ action='store_true',
+ help=_("NUMA affinity policy preferred to schedule this port")
+ )
+ numa_affinity_policy_group.add_argument(
+ '--numa-policy-legacy',
+ action='store_true',
+ help=_("NUMA affinity policy using legacy mode to schedule this port")
+ )
# TODO(abhiraut): Use the SDK resource mapped attribute names once the
@@ -454,12 +480,23 @@ class CreatePort(command.ShowOne):
if parsed_args.qos_policy:
attrs['qos_policy_id'] = client.find_qos_policy(
parsed_args.qos_policy, ignore_missing=False).id
+
+ set_tags_in_post = bool(
+ client.find_extension('tag-ports-during-bulk-creation'))
+ if set_tags_in_post:
+ if parsed_args.no_tag:
+ attrs['tags'] = []
+ if parsed_args.tags:
+ attrs['tags'] = list(set(parsed_args.tags))
+
with common.check_missing_extension_if_error(
self.app.client_manager.network, attrs):
obj = client.create_port(**attrs)
- # tags cannot be set when created, so tags need to be set later.
- _tag.update_tags_for_set(client, obj, parsed_args)
+ if not set_tags_in_post:
+ # tags cannot be set when created, so tags need to be set later.
+ _tag.update_tags_for_set(client, obj, parsed_args)
+
display_columns, columns = _get_columns(obj)
data = utils.get_item_properties(obj, columns, formatters=_formatters)
@@ -515,6 +552,10 @@ class ListPort(command.Lister):
"network:dhcp).")
)
parser.add_argument(
+ '--host',
+ metavar='<host-id>',
+ help=_("List only ports bound to this host ID"))
+ parser.add_argument(
'--network',
metavar='<network>',
help=_("List only ports connected to this network (name or ID)"))
@@ -602,6 +643,8 @@ class ListPort(command.Lister):
server = utils.find_resource(compute_client.servers,
parsed_args.server)
filters['device_id'] = server.id
+ if parsed_args.host:
+ filters['binding:host_id'] = parsed_args.host
if parsed_args.network:
network = network_client.find_network(parsed_args.network,
ignore_missing=False)
@@ -898,6 +941,11 @@ class UnsetPort(command.Command):
action='store_true',
help=_("Clear existing information of data plane status")
)
+ parser.add_argument(
+ '--numa-policy',
+ action='store_true',
+ help=_("Clear existing NUMA affinity policy")
+ )
_tag.add_tag_option_to_parser_for_unset(parser, _('port'))
@@ -953,6 +1001,8 @@ class UnsetPort(command.Command):
attrs['qos_policy_id'] = None
if parsed_args.data_plane_status:
attrs['data_plane_status'] = None
+ if parsed_args.numa_policy:
+ attrs['numa_affinity_policy'] = None
if attrs:
client.update_port(obj, **attrs)
diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py
index 08e9390e..3542a827 100644
--- a/openstackclient/tests/functional/base.py
+++ b/openstackclient/tests/functional/base.py
@@ -19,10 +19,6 @@ from tempest.lib import exceptions
import testtools
-COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
-FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..'))
-ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..'))
-EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples')
ADMIN_CLOUD = os.environ.get('OS_ADMIN_CLOUD', 'devstack-admin')
diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py
index 9c057460..4c2fc0e3 100644
--- a/openstackclient/tests/functional/common/test_quota.py
+++ b/openstackclient/tests/functional/common/test_quota.py
@@ -165,3 +165,47 @@ class QuotaTests(base.TestCase):
# returned attributes
self.assertTrue(cmd_output["key-pairs"] >= 0)
self.assertTrue(cmd_output["snapshots"] >= 0)
+
+ def test_quota_set_force(self):
+ """Test to set instance value by force """
+ json_output = json.loads(self.openstack(
+ 'quota list -f json --detail --compute'
+ ))
+ in_use = limit = None
+ for j in json_output:
+ if j["Resource"] == "instances":
+ in_use = j["In Use"]
+ limit = j["Limit"]
+
+ # Reduce count of in_use
+ in_use = in_use - 1
+ # cannot have negative instances limit
+ if in_use < 0:
+ in_use = 0
+
+ # set the limit by force now
+ self.openstack(
+ 'quota set ' + self.PROJECT_NAME +
+ '--instances ' + str(in_use) + ' --force'
+ )
+ cmd_output = json.loads(self.openstack(
+ 'quota show -f json ' + self.PROJECT_NAME
+ ))
+ self.assertIsNotNone(cmd_output)
+ self.assertEqual(
+ in_use,
+ cmd_output["instances"]
+ )
+
+ # Set instances limit to original limit now
+ self.openstack(
+ 'quota set ' + self.PROJECT_NAME + '--instances ' + str(limit)
+ )
+ cmd_output = json.loads(self.openstack(
+ 'quota show -f json ' + self.PROJECT_NAME
+ ))
+ self.assertIsNotNone(cmd_output)
+ self.assertEqual(
+ limit,
+ cmd_output["instances"]
+ )
diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py
index 6e080e9b..44d9c61f 100644
--- a/openstackclient/tests/functional/compute/v2/test_server.py
+++ b/openstackclient/tests/functional/compute/v2/test_server.py
@@ -16,6 +16,7 @@ import uuid
from tempest.lib import exceptions
+from openstackclient.compute.v2 import server as v2_server
from openstackclient.tests.functional.compute.v2 import common
from openstackclient.tests.functional.volume.v2 import common as volume_common
@@ -230,7 +231,7 @@ class ServerTests(common.ComputeTestCase):
))
# Really, shouldn't this be a list?
self.assertEqual(
- "a='b', c='d'",
+ {'a': 'b', 'c': 'd'},
cmd_output['properties'],
)
@@ -244,7 +245,7 @@ class ServerTests(common.ComputeTestCase):
name
))
self.assertEqual(
- "c='d'",
+ {'c': 'd'},
cmd_output['properties'],
)
@@ -509,6 +510,20 @@ class ServerTests(common.ComputeTestCase):
server['name'],
)
+ # check that image indicates server "booted from volume"
+ self.assertEqual(
+ v2_server.IMAGE_STRING_FOR_BFV,
+ server['image'],
+ )
+ # check server list too
+ servers = json.loads(self.openstack(
+ 'server list -f json'
+ ))
+ self.assertEqual(
+ v2_server.IMAGE_STRING_FOR_BFV,
+ servers[0]['Image']
+ )
+
# check volumes
cmd_output = json.loads(self.openstack(
'volume show -f json ' +
@@ -619,8 +634,8 @@ class ServerTests(common.ComputeTestCase):
server_name
))
volumes_attached = cmd_output['volumes_attached']
- self.assertTrue(volumes_attached.startswith('id='))
- attached_volume_id = volumes_attached.replace('id=', '')
+ self.assertIsNotNone(volumes_attached)
+ attached_volume_id = volumes_attached[0]["id"]
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
@@ -699,8 +714,8 @@ class ServerTests(common.ComputeTestCase):
server_name
))
volumes_attached = cmd_output['volumes_attached']
- self.assertTrue(volumes_attached.startswith('id='))
- attached_volume_id = volumes_attached.replace('id=', '')
+ self.assertIsNotNone(volumes_attached)
+ attached_volume_id = volumes_attached[0]["id"]
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
@@ -773,19 +788,21 @@ class ServerTests(common.ComputeTestCase):
server_name
))
volumes_attached = cmd_output['volumes_attached']
- self.assertTrue(volumes_attached.startswith('id='))
- attached_volume_id = volumes_attached.replace('id=', '')
- # Don't leak the volume when the test exits.
- self.addCleanup(self.openstack, 'volume delete ' + attached_volume_id)
+ self.assertIsNotNone(volumes_attached)
+ attached_volume_id = volumes_attached[0]["id"]
+ for vol in volumes_attached:
+ self.assertIsNotNone(vol['id'])
+ # Don't leak the volume when the test exits.
+ self.addCleanup(self.openstack, 'volume delete ' + vol['id'])
# Since the server is volume-backed the GET /servers/{server_id}
- # response will have image=''.
- self.assertEqual('', cmd_output['image'])
+ # response will have image='N/A (booted from volume)'.
+ self.assertEqual(v2_server.IMAGE_STRING_FOR_BFV, cmd_output['image'])
# check the volume that attached on server
cmd_output = json.loads(self.openstack(
'volume show -f json ' +
- attached_volume_id
+ volumes_attached[0]["id"]
))
# The volume size should be what we specified on the command line.
self.assertEqual(1, int(cmd_output['size']))
@@ -879,14 +896,21 @@ class ServerTests(common.ComputeTestCase):
self.assertIsNotNone(server['id'])
self.assertEqual(server_name, server['name'])
- self.assertIn(str(security_group1['id']), server['security_groups'])
- self.assertIn(str(security_group2['id']), server['security_groups'])
+ sec_grp = ""
+ for sec in server['security_groups']:
+ sec_grp += sec['name']
+ self.assertIn(str(security_group1['id']), sec_grp)
+ self.assertIn(str(security_group2['id']), sec_grp)
self.wait_for_status(server_name, 'ACTIVE')
server = json.loads(self.openstack(
'server show -f json ' + server_name
))
- self.assertIn(sg_name1, server['security_groups'])
- self.assertIn(sg_name2, server['security_groups'])
+ # check if security group exists in list
+ sec_grp = ""
+ for sec in server['security_groups']:
+ sec_grp += sec['name']
+ self.assertIn(sg_name1, sec_grp)
+ self.assertIn(sg_name2, sec_grp)
def test_server_create_with_empty_network_option_latest(self):
"""Test server create with empty network option in nova 2.latest."""
diff --git a/openstackclient/tests/functional/examples/__init__.py b/openstackclient/tests/functional/examples/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/openstackclient/tests/functional/examples/__init__.py
+++ /dev/null
diff --git a/openstackclient/tests/functional/examples/test_examples.py b/openstackclient/tests/functional/examples/test_examples.py
deleted file mode 100644
index 031f036a..00000000
--- a/openstackclient/tests/functional/examples/test_examples.py
+++ /dev/null
@@ -1,28 +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.
-
-from openstackclient.tests.functional import base
-
-
-class ExampleTests(base.TestCase):
- """Functional tests for running examples."""
-
- def test_common(self):
- # NOTE(stevemar): If an examples has a non-zero return
- # code, then execute will raise an error by default.
- base.execute('python', base.EXAMPLE_DIR + '/common.py --debug')
-
- def test_object_api(self):
- base.execute('python', base.EXAMPLE_DIR + '/object_api.py --debug')
-
- def test_osc_lib(self):
- base.execute('python', base.EXAMPLE_DIR + '/osc-lib.py --debug')
diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py
index bd59ca77..6504c5b0 100644
--- a/openstackclient/tests/unit/common/test_quota.py
+++ b/openstackclient/tests/unit/common/test_quota.py
@@ -392,6 +392,29 @@ class TestQuotaList(TestQuota):
parsed_args,
)
+ def test_quota_list_compute_by_project(self):
+ # Two projects with non-default quotas
+ self.compute.quotas.get = mock.Mock(
+ side_effect=self.compute_quotas,
+ )
+
+ arglist = [
+ '--compute',
+ '--project', self.projects[0].name,
+ ]
+ verifylist = [
+ ('compute', True),
+ ('project', self.projects[0].name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+ ret_quotas = list(data)
+
+ self.assertEqual(self.compute_column_header, columns)
+ self.assertEqual(self.compute_reference_data, ret_quotas[0])
+ self.assertEqual(1, len(ret_quotas))
+
def test_quota_list_network(self):
# Two projects with non-default quotas
self.network.get_quota = mock.Mock(
@@ -461,6 +484,29 @@ class TestQuotaList(TestQuota):
self.assertEqual(self.network_reference_data, ret_quotas[0])
self.assertEqual(1, len(ret_quotas))
+ def test_quota_list_network_by_project(self):
+ # Two projects with non-default quotas
+ self.network.get_quota = mock.Mock(
+ side_effect=self.network_quotas,
+ )
+
+ arglist = [
+ '--network',
+ '--project', self.projects[0].name,
+ ]
+ verifylist = [
+ ('network', True),
+ ('project', self.projects[0].name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+ ret_quotas = list(data)
+
+ self.assertEqual(self.network_column_header, columns)
+ self.assertEqual(self.network_reference_data, ret_quotas[0])
+ self.assertEqual(1, len(ret_quotas))
+
def test_quota_list_volume(self):
# Two projects with non-default quotas
self.volume.quotas.get = mock.Mock(
@@ -530,6 +576,29 @@ class TestQuotaList(TestQuota):
self.assertEqual(self.volume_reference_data, ret_quotas[0])
self.assertEqual(1, len(ret_quotas))
+ def test_quota_list_volume_by_project(self):
+ # Two projects with non-default quotas
+ self.volume.quotas.get = mock.Mock(
+ side_effect=self.volume_quotas,
+ )
+
+ arglist = [
+ '--volume',
+ '--project', self.projects[0].name,
+ ]
+ verifylist = [
+ ('volume', True),
+ ('project', self.projects[0].name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+ ret_quotas = list(data)
+
+ self.assertEqual(self.volume_column_header, columns)
+ self.assertEqual(self.volume_reference_data, ret_quotas[0])
+ self.assertEqual(1, len(ret_quotas))
+
class TestQuotaSet(TestQuota):
@@ -831,6 +900,56 @@ class TestQuotaSet(TestQuota):
self.assertNotCalled(self.network_mock.update_quota)
self.assertIsNone(result)
+ def test_quota_set_with_force(self):
+ arglist = [
+ '--cores', str(compute_fakes.core_num),
+ '--ram', str(compute_fakes.ram_num),
+ '--instances', str(compute_fakes.instance_num),
+ '--volumes', str(volume_fakes.QUOTA['volumes']),
+ '--subnets', str(network_fakes.QUOTA['subnet']),
+ '--force',
+ self.projects[0].name,
+ ]
+ verifylist = [
+ ('cores', compute_fakes.core_num),
+ ('ram', compute_fakes.ram_num),
+ ('instances', compute_fakes.instance_num),
+ ('volumes', volume_fakes.QUOTA['volumes']),
+ ('subnet', network_fakes.QUOTA['subnet']),
+ ('force', True),
+ ('project', self.projects[0].name),
+ ]
+ self.app.client_manager.network_endpoint_enabled = True
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ kwargs_compute = {
+ 'cores': compute_fakes.core_num,
+ 'ram': compute_fakes.ram_num,
+ 'instances': compute_fakes.instance_num,
+ 'force': True,
+ }
+ kwargs_volume = {
+ 'volumes': volume_fakes.QUOTA['volumes'],
+ }
+ kwargs_network = {
+ 'subnet': network_fakes.QUOTA['subnet'],
+ }
+ self.compute_quotas_mock.update.assert_called_once_with(
+ self.projects[0].id,
+ **kwargs_compute
+ )
+ self.volume_quotas_mock.update.assert_called_once_with(
+ self.projects[0].id,
+ **kwargs_volume
+ )
+ self.network_mock.update_quota.assert_called_once_with(
+ self.projects[0].id,
+ **kwargs_network
+ )
+ self.assertIsNone(result)
+
class TestQuotaShow(TestQuota):
diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py
index 6e12f735..31430984 100644
--- a/openstackclient/tests/unit/compute/v2/fakes.py
+++ b/openstackclient/tests/unit/compute/v2/fakes.py
@@ -1244,7 +1244,7 @@ class FakeServerGroup(object):
"""Fake one server group"""
@staticmethod
- def create_one_server_group(attrs=None):
+ def _create_one_server_group(attrs=None):
"""Create a fake server group
:param Dictionary attrs:
@@ -1261,7 +1261,6 @@ class FakeServerGroup(object):
'members': [],
'metadata': {},
'name': 'server-group-name-' + uuid.uuid4().hex,
- 'policies': [],
'project_id': 'server-group-project-id-' + uuid.uuid4().hex,
'user_id': 'server-group-user-id-' + uuid.uuid4().hex,
}
@@ -1274,6 +1273,38 @@ class FakeServerGroup(object):
loaded=True)
return server_group
+ @staticmethod
+ def create_one_server_group(attrs=None):
+ """Create a fake server group
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :return:
+ A FakeResource object, with id and other attributes
+ """
+ if attrs is None:
+ attrs = {}
+ attrs.setdefault('policies', ['policy1', 'policy2'])
+ return FakeServerGroup._create_one_server_group(attrs)
+
+
+class FakeServerGroupV264(object):
+ """Fake one server group fo API >= 2.64"""
+
+ @staticmethod
+ def create_one_server_group(attrs=None):
+ """Create a fake server group
+
+ :param Dictionary attrs:
+ A dictionary with all attributes
+ :return:
+ A FakeResource object, with id and other attributes
+ """
+ if attrs is None:
+ attrs = {}
+ attrs.setdefault('policy', 'policy1')
+ return FakeServerGroup._create_one_server_group(attrs)
+
class FakeUsage(object):
"""Fake one or more usage."""
diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py
index fe7ce174..4732cc82 100644
--- a/openstackclient/tests/unit/compute/v2/test_flavor.py
+++ b/openstackclient/tests/unit/compute/v2/test_flavor.py
@@ -749,6 +749,42 @@ class TestFlavorSet(TestFlavor):
self.assertRaises(exceptions.CommandError, self.cmd.take_action,
parsed_args)
+ def test_flavor_set_description_using_name_api_newer(self):
+ arglist = [
+ '--description', 'description',
+ self.flavor.name,
+ ]
+ verifylist = [
+ ('description', 'description'),
+ ('flavor', self.flavor.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.app.client_manager.compute.api_version = 2.55
+ with mock.patch.object(novaclient.api_versions,
+ 'APIVersion',
+ return_value=2.55):
+ result = self.cmd.take_action(parsed_args)
+ self.flavors_mock.update.assert_called_with(
+ flavor=self.flavor.id, description='description')
+ self.assertIsNone(result)
+
+ def test_flavor_set_description_using_name_api_older(self):
+ arglist = [
+ '--description', 'description',
+ self.flavor.name,
+ ]
+ verifylist = [
+ ('description', 'description'),
+ ('flavor', self.flavor.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.app.client_manager.compute.api_version = 2.54
+ with mock.patch.object(novaclient.api_versions,
+ 'APIVersion',
+ return_value=2.55):
+ self.assertRaises(exceptions.CommandError, self.cmd.take_action,
+ parsed_args)
+
class TestFlavorShow(TestFlavor):
diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py
index 7e4c71c5..2da527c6 100644
--- a/openstackclient/tests/unit/compute/v2/test_server.py
+++ b/openstackclient/tests/unit/compute/v2/test_server.py
@@ -2609,7 +2609,7 @@ class TestServerList(TestServer):
s.status,
server._format_servers_list_networks(s.networks),
# Image will be an empty string if boot-from-volume
- self.image.name if s.image else s.image,
+ self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
self.flavor.name,
))
self.data_long.append((
@@ -2622,8 +2622,8 @@ class TestServerList(TestServer):
),
server._format_servers_list_networks(s.networks),
# Image will be an empty string if boot-from-volume
- self.image.name if s.image else s.image,
- s.image['id'] if s.image else s.image,
+ self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
+ s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
self.flavor.name,
s.flavor['id'],
getattr(s, 'OS-EXT-AZ:availability_zone'),
@@ -2636,7 +2636,7 @@ class TestServerList(TestServer):
s.status,
server._format_servers_list_networks(s.networks),
# Image will be an empty string if boot-from-volume
- s.image['id'] if s.image else s.image,
+ s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
s.flavor['id']
))
@@ -5166,6 +5166,8 @@ class TestServerGeneral(TestServer):
'tenant_id': u'tenant-id-xxx',
'networks': {u'public': [u'10.20.30.40', u'2001:db8::f']},
'links': u'http://xxx.yyy.com',
+ 'properties': '',
+ 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}],
}
_server = compute_fakes.FakeServer.create_one_server(attrs=server_info)
find_resource.side_effect = [_server, _flavor]
@@ -5182,6 +5184,7 @@ class TestServerGeneral(TestServer):
'properties': '',
'OS-EXT-STS:power_state': server._format_servers_list_power_state(
getattr(_server, 'OS-EXT-STS:power_state')),
+ 'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}],
}
# Call _prep_server_detail().
diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py
index b9bb79f6..359cd2bd 100644
--- a/openstackclient/tests/unit/compute/v2/test_server_group.py
+++ b/openstackclient/tests/unit/compute/v2/test_server_group.py
@@ -54,6 +54,33 @@ class TestServerGroup(compute_fakes.TestComputev2):
self.server_groups_mock.reset_mock()
+class TestServerGroupV264(TestServerGroup):
+
+ fake_server_group = \
+ compute_fakes.FakeServerGroupV264.create_one_server_group()
+
+ columns = (
+ 'id',
+ 'members',
+ 'name',
+ 'policy',
+ 'project_id',
+ 'user_id',
+ )
+
+ data = (
+ fake_server_group.id,
+ utils.format_list(fake_server_group.members),
+ fake_server_group.name,
+ fake_server_group.policy,
+ fake_server_group.project_id,
+ fake_server_group.user_id,
+ )
+
+ def setUp(self):
+ super(TestServerGroupV264, self).setUp()
+
+
class TestServerGroupCreate(TestServerGroup):
def setUp(self):
@@ -253,6 +280,76 @@ class TestServerGroupList(TestServerGroup):
self.assertEqual(self.list_data_long, tuple(data))
+class TestServerGroupListV264(TestServerGroupV264):
+
+ list_columns = (
+ 'ID',
+ 'Name',
+ 'Policy',
+ )
+
+ list_columns_long = (
+ 'ID',
+ 'Name',
+ 'Policy',
+ 'Members',
+ 'Project Id',
+ 'User Id',
+ )
+
+ list_data = ((
+ TestServerGroupV264.fake_server_group.id,
+ TestServerGroupV264.fake_server_group.name,
+ TestServerGroupV264.fake_server_group.policy,
+ ),)
+
+ list_data_long = ((
+ TestServerGroupV264.fake_server_group.id,
+ TestServerGroupV264.fake_server_group.name,
+ TestServerGroupV264.fake_server_group.policy,
+ utils.format_list(TestServerGroupV264.fake_server_group.members),
+ TestServerGroupV264.fake_server_group.project_id,
+ TestServerGroupV264.fake_server_group.user_id,
+ ),)
+
+ def setUp(self):
+ super(TestServerGroupListV264, self).setUp()
+
+ self.server_groups_mock.list.return_value = [self.fake_server_group]
+ self.cmd = server_group.ListServerGroup(self.app, None)
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.64')
+
+ def test_server_group_list(self):
+ arglist = []
+ verifylist = [
+ ('all_projects', False),
+ ('long', False),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+ self.server_groups_mock.list.assert_called_once_with(False)
+
+ self.assertEqual(self.list_columns, columns)
+ self.assertEqual(self.list_data, tuple(data))
+
+ def test_server_group_list_with_all_projects_and_long(self):
+ arglist = [
+ '--all-projects',
+ '--long',
+ ]
+ verifylist = [
+ ('all_projects', True),
+ ('long', True),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+ self.server_groups_mock.list.assert_called_once_with(True)
+
+ self.assertEqual(self.list_columns_long, columns)
+ self.assertEqual(self.list_data_long, tuple(data))
+
+
class TestServerGroupShow(TestServerGroup):
def setUp(self):
diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py
index 544da7c1..774b2c2b 100644
--- a/openstackclient/tests/unit/identity/v3/test_role.py
+++ b/openstackclient/tests/unit/identity/v3/test_role.py
@@ -19,6 +19,7 @@ from unittest import mock
from osc_lib import exceptions
from osc_lib import utils
+from openstackclient.identity import common
from openstackclient.identity.v3 import role
from openstackclient.tests.unit import fakes
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
@@ -102,6 +103,40 @@ class TestRoleAdd(TestRole):
# Get the command object to test
self.cmd = role.AddRole(self.app, None)
+ def test_role_add_user_system(self):
+ arglist = [
+ '--user', identity_fakes.user_name,
+ '--system', 'all',
+ identity_fakes.role_name,
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', identity_fakes.user_name),
+ ('group', None),
+ ('system', 'all'),
+ ('domain', None),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'user': identity_fakes.user_id,
+ 'system': 'all',
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.grant(role, user=, group=, domain=, project=)
+ self.roles_mock.grant.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_add_user_domain(self):
arglist = [
'--user', identity_fakes.user_name,
@@ -168,6 +203,40 @@ class TestRoleAdd(TestRole):
)
self.assertIsNone(result)
+ def test_role_add_group_system(self):
+ arglist = [
+ '--group', identity_fakes.group_name,
+ '--system', 'all',
+ identity_fakes.role_name,
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', None),
+ ('group', identity_fakes.group_name),
+ ('system', 'all'),
+ ('domain', None),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'group': identity_fakes.group_id,
+ 'system': 'all',
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.grant(role, user=, group=, domain=, project=)
+ self.roles_mock.grant.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_add_group_domain(self):
arglist = [
'--group', identity_fakes.group_name,
@@ -744,6 +813,81 @@ class TestRoleRemove(TestRole):
# Get the command object to test
self.cmd = role.RemoveRole(self.app, None)
+ def test_role_remove_user_system(self):
+ arglist = [
+ '--user', identity_fakes.user_name,
+ '--system', 'all',
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', identity_fakes.user_name),
+ ('group', None),
+ ('system', 'all'),
+ ('domain', None),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'user': identity_fakes.user_id,
+ 'system': 'all',
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ @mock.patch.object(common, 'find_user')
+ def test_role_remove_non_existent_user_system(self, find_mock):
+ # Simulate the user not being in keystone, the client should gracefully
+ # handle this exception and send the request to remove the role since
+ # keystone supports removing role assignments with non-existent actors
+ # (e.g., users or groups).
+ find_mock.side_effect = exceptions.CommandError
+
+ arglist = [
+ '--user', identity_fakes.user_id,
+ '--system', 'all',
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', identity_fakes.user_id),
+ ('group', None),
+ ('system', 'all'),
+ ('domain', None),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'user': identity_fakes.user_id,
+ 'system': 'all',
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_remove_user_domain(self):
arglist = [
'--user', identity_fakes.user_name,
@@ -777,6 +921,46 @@ class TestRoleRemove(TestRole):
)
self.assertIsNone(result)
+ @mock.patch.object(common, 'find_user')
+ def test_role_remove_non_existent_user_domain(self, find_mock):
+ # Simulate the user not being in keystone, the client the gracefully
+ # handle this exception and send the request to remove the role since
+ # keystone will validate.
+ find_mock.side_effect = exceptions.CommandError
+
+ arglist = [
+ '--user', identity_fakes.user_id,
+ '--domain', identity_fakes.domain_name,
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', identity_fakes.user_id),
+ ('group', None),
+ ('system', None),
+ ('domain', identity_fakes.domain_name),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'user': identity_fakes.user_id,
+ 'domain': identity_fakes.domain_id,
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_remove_user_project(self):
arglist = [
'--user', identity_fakes.user_name,
@@ -810,6 +994,121 @@ class TestRoleRemove(TestRole):
)
self.assertIsNone(result)
+ @mock.patch.object(common, 'find_user')
+ def test_role_remove_non_existent_user_project(self, find_mock):
+ # Simulate the user not being in keystone, the client the gracefully
+ # handle this exception and send the request to remove the role since
+ # keystone will validate.
+ find_mock.side_effect = exceptions.CommandError
+
+ arglist = [
+ '--user', identity_fakes.user_id,
+ '--project', identity_fakes.project_name,
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', identity_fakes.user_id),
+ ('group', None),
+ ('system', None),
+ ('domain', None),
+ ('project', identity_fakes.project_name),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'user': identity_fakes.user_id,
+ 'project': identity_fakes.project_id,
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ def test_role_remove_group_system(self):
+ arglist = [
+ '--group', identity_fakes.group_name,
+ '--system', 'all',
+ identity_fakes.role_name,
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', None),
+ ('group', identity_fakes.group_name),
+ ('system', 'all'),
+ ('domain', None),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'group': identity_fakes.group_id,
+ 'system': 'all',
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
+ @mock.patch.object(common, 'find_group')
+ def test_role_remove_non_existent_group_system(self, find_mock):
+ # Simulate the user not being in keystone, the client the gracefully
+ # handle this exception and send the request to remove the role since
+ # keystone will validate.
+ find_mock.side_effect = exceptions.CommandError
+
+ arglist = [
+ '--group', identity_fakes.group_id,
+ '--system', 'all',
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', None),
+ ('group', identity_fakes.group_id),
+ ('system', 'all'),
+ ('domain', None),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'group': identity_fakes.group_id,
+ 'system': 'all',
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_remove_group_domain(self):
arglist = [
'--group', identity_fakes.group_name,
@@ -844,6 +1143,46 @@ class TestRoleRemove(TestRole):
)
self.assertIsNone(result)
+ @mock.patch.object(common, 'find_group')
+ def test_role_remove_non_existent_group_domain(self, find_mock):
+ # Simulate the user not being in keystone, the client the gracefully
+ # handle this exception and send the request to remove the role since
+ # keystone will validate.
+ find_mock.side_effect = exceptions.CommandError
+
+ arglist = [
+ '--group', identity_fakes.group_id,
+ '--domain', identity_fakes.domain_name,
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', None),
+ ('group', identity_fakes.group_id),
+ ('system', None),
+ ('domain', identity_fakes.domain_name),
+ ('project', None),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'group': identity_fakes.group_id,
+ 'domain': identity_fakes.domain_id,
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_remove_group_project(self):
arglist = [
'--group', identity_fakes.group_name,
@@ -877,6 +1216,46 @@ class TestRoleRemove(TestRole):
)
self.assertIsNone(result)
+ @mock.patch.object(common, 'find_group')
+ def test_role_remove_non_existent_group_project(self, find_mock):
+ # Simulate the user not being in keystone, the client the gracefully
+ # handle this exception and send the request to remove the role since
+ # keystone will validate.
+ find_mock.side_effect = exceptions.CommandError
+
+ arglist = [
+ '--group', identity_fakes.group_id,
+ '--project', identity_fakes.project_name,
+ identity_fakes.role_name
+ ]
+ if self._is_inheritance_testcase():
+ arglist.append('--inherited')
+ verifylist = [
+ ('user', None),
+ ('group', identity_fakes.group_id),
+ ('system', None),
+ ('domain', None),
+ ('project', identity_fakes.project_name),
+ ('role', identity_fakes.role_name),
+ ('inherited', self._is_inheritance_testcase()),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+
+ # Set expected values
+ kwargs = {
+ 'group': identity_fakes.group_id,
+ 'project': identity_fakes.project_id,
+ 'os_inherit_extension_inherited': self._is_inheritance_testcase(),
+ }
+ # RoleManager.revoke(role, user=, group=, domain=, project=)
+ self.roles_mock.revoke.assert_called_with(
+ identity_fakes.role_id,
+ **kwargs
+ )
+ self.assertIsNone(result)
+
def test_role_remove_domain_role_on_group_domain(self):
self.roles_mock.get.return_value = fakes.FakeResource(
None,
diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py
index a021cfc7..310f6b76 100644
--- a/openstackclient/tests/unit/image/v2/test_image.py
+++ b/openstackclient/tests/unit/image/v2/test_image.py
@@ -271,6 +271,28 @@ class TestImageCreate(TestImage):
exceptions.CommandError,
self.cmd.take_action, parsed_args)
+ @mock.patch('sys.stdin', side_effect=[None])
+ def test_image_create_import(self, raw_input):
+
+ arglist = [
+ '--import',
+ self.new_image.name,
+ ]
+ verifylist = [
+ ('name', self.new_image.name),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+
+ # ImageManager.create(name=, **)
+ self.client.create_image.assert_called_with(
+ name=self.new_image.name,
+ container_format=image.DEFAULT_CONTAINER_FORMAT,
+ disk_format=image.DEFAULT_DISK_FORMAT,
+ use_import=True
+ )
+
class TestAddProjectToImage(TestImage):
diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py
index cef0a11c..3df4042c 100644
--- a/openstackclient/tests/unit/network/v2/fakes.py
+++ b/openstackclient/tests/unit/network/v2/fakes.py
@@ -634,6 +634,7 @@ class FakePort(object):
'mac_address': 'fa:16:3e:a9:4e:72',
'name': 'port-name-' + uuid.uuid4().hex,
'network_id': 'network-id-' + uuid.uuid4().hex,
+ 'numa_affinity_policy': 'required',
'port_security_enabled': True,
'security_group_ids': [],
'status': 'ACTIVE',
diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py
index b1a18da6..f729b552 100644
--- a/openstackclient/tests/unit/network/v2/test_port.py
+++ b/openstackclient/tests/unit/network/v2/test_port.py
@@ -59,6 +59,7 @@ class TestPort(network_fakes.TestNetworkV2):
'mac_address',
'name',
'network_id',
+ 'numa_affinity_policy',
'port_security_enabled',
'project_id',
'qos_network_policy_id',
@@ -90,6 +91,7 @@ class TestPort(network_fakes.TestNetworkV2):
fake_port.mac_address,
fake_port.name,
fake_port.network_id,
+ fake_port.numa_affinity_policy,
fake_port.port_security_enabled,
fake_port.project_id,
fake_port.qos_network_policy_id,
@@ -119,6 +121,7 @@ class TestCreatePort(TestPort):
self.network.find_network = mock.Mock(return_value=fake_net)
self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet()
self.network.find_subnet = mock.Mock(return_value=self.fake_subnet)
+ self.network.find_extension = mock.Mock(return_value=[])
# Get the command object to test
self.cmd = port.CreatePort(self.app, self.namespace)
@@ -534,7 +537,7 @@ class TestCreatePort(TestPort):
'name': 'test-port',
})
- def _test_create_with_tag(self, add_tags=True):
+ def _test_create_with_tag(self, add_tags=True, add_tags_in_post=True):
arglist = [
'--network', self._port.network_id,
'test-port',
@@ -553,28 +556,59 @@ class TestCreatePort(TestPort):
else:
verifylist.append(('no_tag', True))
+ self.network.find_extension = mock.Mock(return_value=add_tags_in_post)
+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = (self.cmd.take_action(parsed_args))
- self.network.create_port.assert_called_once_with(
- admin_state_up=True,
- network_id=self._port.network_id,
- name='test-port'
- )
- if add_tags:
- self.network.set_tags.assert_called_once_with(
- self._port,
- tests_utils.CompareBySet(['red', 'blue']))
+ args = {
+ 'admin_state_up': True,
+ 'network_id': self._port.network_id,
+ 'name': 'test-port',
+ }
+ if add_tags_in_post:
+ if add_tags:
+ args['tags'] = sorted(['red', 'blue'])
+ else:
+ args['tags'] = []
+ self.network.create_port.assert_called_once()
+ # Now we need to verify if arguments to call create_port are as
+ # expected,
+ # But we can't simply use assert_called_once_with() method because
+ # duplicates from 'tags' are removed with
+ # list(set(parsed_args.tags)) and that don't quarantee order of
+ # tags list which is used to call create_port().
+ create_port_call_kwargs = self.network.create_port.call_args[1]
+ create_port_call_kwargs['tags'] = sorted(
+ create_port_call_kwargs['tags'])
+ self.assertDictEqual(args, create_port_call_kwargs)
else:
- self.assertFalse(self.network.set_tags.called)
+ self.network.create_port.assert_called_once_with(
+ admin_state_up=True,
+ network_id=self._port.network_id,
+ name='test-port'
+ )
+ if add_tags:
+ self.network.set_tags.assert_called_once_with(
+ self._port,
+ tests_utils.CompareBySet(['red', 'blue']))
+ else:
+ self.assertFalse(self.network.set_tags.called)
+
self.assertEqual(self.columns, columns)
self.assertItemEqual(self.data, data)
def test_create_with_tags(self):
- self._test_create_with_tag(add_tags=True)
+ self._test_create_with_tag(add_tags=True, add_tags_in_post=True)
def test_create_with_no_tag(self):
- self._test_create_with_tag(add_tags=False)
+ self._test_create_with_tag(add_tags=False, add_tags_in_post=True)
+
+ def test_create_with_tags_using_put(self):
+ self._test_create_with_tag(add_tags=True, add_tags_in_post=False)
+
+ def test_create_with_no_tag_using_put(self):
+ self._test_create_with_tag(add_tags=False, add_tags_in_post=False)
def _test_create_with_uplink_status_propagation(self, enable=True):
arglist = [
@@ -655,6 +689,50 @@ class TestCreatePort(TestPort):
'name': 'test-port',
})
+ def _test_create_with_numa_affinity_policy(self, policy=None):
+ arglist = [
+ '--network', self._port.network_id,
+ 'test-port',
+ ]
+ if policy:
+ arglist += ['--numa-policy-%s' % policy]
+
+ numa_affinity_policy = None if not policy else policy
+ verifylist = [
+ ('network', self._port.network_id,),
+ ('name', 'test-port'),
+ ]
+ if policy:
+ verifylist.append(('numa_policy_%s' % policy, True))
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = (self.cmd.take_action(parsed_args))
+
+ create_args = {
+ 'admin_state_up': True,
+ 'network_id': self._port.network_id,
+ 'name': 'test-port',
+ }
+ if numa_affinity_policy:
+ create_args['numa_affinity_policy'] = numa_affinity_policy
+ self.network.create_port.assert_called_once_with(**create_args)
+
+ self.assertEqual(self.columns, columns)
+ self.assertItemEqual(self.data, data)
+
+ def test_create_with_numa_affinity_policy_required(self):
+ self._test_create_with_numa_affinity_policy(policy='required')
+
+ def test_create_with_numa_affinity_policy_preferred(self):
+ self._test_create_with_numa_affinity_policy(policy='preferred')
+
+ def test_create_with_numa_affinity_policy_legacy(self):
+ self._test_create_with_numa_affinity_policy(policy='legacy')
+
+ def test_create_with_numa_affinity_policy_null(self):
+ self._test_create_with_numa_affinity_policy()
+
class TestDeletePort(TestPort):
@@ -1054,6 +1132,22 @@ class TestListPort(TestPort):
self.assertEqual(self.columns_long, columns)
self.assertListItemEqual(self.data_long, list(data))
+ def test_port_list_host(self):
+ arglist = [
+ '--host', 'foobar',
+ ]
+ verifylist = [
+ ('host', 'foobar'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ columns, data = self.cmd.take_action(parsed_args)
+ filters = {'binding:host_id': 'foobar'}
+
+ self.network.ports.assert_called_once_with(**filters)
+ self.assertEqual(self.columns, columns)
+ self.assertListItemEqual(self.data, list(data))
+
def test_port_list_project(self):
project = identity_fakes.FakeProject.create_one_project()
self.projects_mock.get.return_value = project
@@ -1652,6 +1746,32 @@ class TestSetPort(TestPort):
def test_set_with_no_tag(self):
self._test_set_tags(with_tags=False)
+ def _test_create_with_numa_affinity_policy(self, policy):
+ arglist = [
+ '--numa-policy-%s' % policy,
+ self._port.id,
+ ]
+ verifylist = [
+ ('numa_policy_%s' % policy, True),
+ ('port', self._port.id,)
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ self.cmd.take_action(parsed_args)
+
+ self.network.update_port.assert_called_once_with(
+ self._port, **{'numa_affinity_policy': policy})
+
+ def test_create_with_numa_affinity_policy_required(self):
+ self._test_create_with_numa_affinity_policy('required')
+
+ def test_create_with_numa_affinity_policy_preferred(self):
+ self._test_create_with_numa_affinity_policy('preferred')
+
+ def test_create_with_numa_affinity_policy_legacy(self):
+ self._test_create_with_numa_affinity_policy('legacy')
+
class TestShowPort(TestPort):
@@ -1907,3 +2027,26 @@ class TestUnsetPort(TestPort):
def test_unset_with_all_tag(self):
self._test_unset_tags(with_tags=False)
+
+ def test_unset_numa_affinity_policy(self):
+ _fake_port = network_fakes.FakePort.create_one_port(
+ {'numa_affinity_policy': 'required'})
+ self.network.find_port = mock.Mock(return_value=_fake_port)
+ arglist = [
+ '--numa-policy',
+ _fake_port.name,
+ ]
+ verifylist = [
+ ('numa_policy', True),
+ ('port', _fake_port.name),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ result = self.cmd.take_action(parsed_args)
+
+ attrs = {
+ 'numa_affinity_policy': None,
+ }
+
+ self.network.update_port.assert_called_once_with(_fake_port, **attrs)
+ self.assertIsNone(result)
diff --git a/releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml b/releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml
new file mode 100644
index 00000000..fbd29bcd
--- /dev/null
+++ b/releasenotes/notes/add-image-import-flag-899869dc5a92aea7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Added ``--import`` flag to ``openstack image create`` to allow
+ the user to force use of the image import codepath.
diff --git a/releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml b/releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml
new file mode 100644
index 00000000..0fbc54b1
--- /dev/null
+++ b/releasenotes/notes/add-port-numa-affinity-policy-4706b0f9485a5d4d.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add NUMA affinity policy to ``port create``, ``port set`` and
+ ``port unset`` commands.
diff --git a/releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml b/releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml
new file mode 100644
index 00000000..ecc58c8a
--- /dev/null
+++ b/releasenotes/notes/bug-2006635-3110f7a87a186e62.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ You can now remove role assignments from keystone that reference non-existent
+ users or groups.
+
+ [Bug `2006635 <https://storyboard.openstack.org/#!/story/2006635>`_]
diff --git a/releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml b/releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml
new file mode 100644
index 00000000..cb25cff1
--- /dev/null
+++ b/releasenotes/notes/entrypoint-3.8-0597d159889042f7.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fixes an issue with python 3.8 and entrypoint loading where the
+ new builtin importlib entrypoint support had a different
+ attribute api than expected.
diff --git a/releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml b/releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml
new file mode 100644
index 00000000..87d6f18b
--- /dev/null
+++ b/releasenotes/notes/fix-story-2007890-0974f3e69f26801e.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ While uploading a signed image, a private key to sign that image must be
+ specified. The CLI client asks for the password of that private key. Due
+ to wrong encoding handling while using Python 3, the password is not
+ accepted, whether it is correct or not.
diff --git a/releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml b/releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml
new file mode 100644
index 00000000..e1980d11
--- /dev/null
+++ b/releasenotes/notes/force-flag-openstackclient-c172de2717e5cfac.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - Add ``--force`` options to the ``openstack quota set``
+ command. The compute service allows us to to force set a quota, setting a
+ quota value that is less than the amount of the resource currently
+ consumed. Expose this feature by way of a ``--force`` boolean parameter.
diff --git a/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml b/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml
new file mode 100644
index 00000000..3a0155a1
--- /dev/null
+++ b/releasenotes/notes/security-grp-json-fix.yaml-2af1f48a48034d64.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+ - The ``openstack server show -f json`` command was not outputting
+ json for security groups, volumes and properties properly.
diff --git a/releasenotes/notes/task-40279-eb0d718ac1959c50.yaml b/releasenotes/notes/task-40279-eb0d718ac1959c50.yaml
new file mode 100644
index 00000000..aedb3ea9
--- /dev/null
+++ b/releasenotes/notes/task-40279-eb0d718ac1959c50.yaml
@@ -0,0 +1,5 @@
+---
+fixes:
+ - |
+ Makes ``volume backup record`` commands available in Volume API v3.
+ `Task 40279 <https://storyboard.openstack.org/#!/story/2007896>`__
diff --git a/requirements.txt b/requirements.txt
index 3aac36af..2b7976e5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,10 +5,11 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
six>=1.10.0 # MIT
cliff!=2.9.0,>=2.8.0 # Apache-2.0
-openstacksdk>=0.44.0 # Apache-2.0
+openstacksdk>=0.48.0 # Apache-2.0
osc-lib>=2.0.0 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
python-keystoneclient>=3.22.0 # Apache-2.0
python-novaclient>=15.1.0 # Apache-2.0
python-cinderclient>=3.3.0 # Apache-2.0
+stevedore>=2.0.1 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 273529b1..1d9d6df6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -685,6 +685,9 @@ openstack.volume.v3 =
volume_backup_set = openstackclient.volume.v2.volume_backup:SetVolumeBackup
volume_backup_show = openstackclient.volume.v2.volume_backup:ShowVolumeBackup
+ volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord
+ volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord
+
volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost
volume_snapshot_create = openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot
@@ -718,17 +721,3 @@ openstack.volume.v3 =
volume_transfer_request_delete = openstackclient.volume.v2.volume_transfer_request:DeleteTransferRequest
volume_transfer_request_list = openstackclient.volume.v2.volume_transfer_request:ListTransferRequest
volume_transfer_request_show = openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest
-
-[extract_messages]
-keywords = _ gettext ngettext l_ lazy_gettext
-mapping_file = babel.cfg
-output_file = openstackclient/locale/openstackclient.pot
-
-[update_catalog]
-domain = openstackclient
-output_dir = openstackclient/locale
-input_file = openstackclient/locale/openstackclient.pot
-
-[compile_catalog]
-directory = openstackclient/locale
-domain = openstackclient
diff --git a/test-requirements.txt b/test-requirements.txt
index f2b6a134..3dce687b 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -8,7 +8,6 @@ flake8-import-order>=0.13 # LGPLv3
oslotest>=3.2.0 # Apache-2.0
requests>=2.14.2 # Apache-2.0
requests-mock>=1.2.0 # Apache-2.0
-stevedore>=1.20.0 # Apache-2.0
stestr>=1.0.0 # Apache-2.0
testtools>=2.2.0 # MIT
tempest>=17.1.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index 3a2a307d..fdff6539 100644
--- a/tox.ini
+++ b/tox.ini
@@ -111,7 +111,6 @@ commands =
[testenv:docs]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
- -r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html