summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml3
-rw-r--r--lower-constraints.txt142
-rw-r--r--openstackclient/compute/v2/server.py192
-rw-r--r--openstackclient/compute/v2/server_group.py63
-rw-r--r--openstackclient/image/v1/image.py6
-rw-r--r--openstackclient/image/v2/image.py6
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py316
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server_group.py90
-rw-r--r--openstackclient/tests/unit/image/v2/test_image.py3
-rw-r--r--releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml5
-rw-r--r--releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml8
-rw-r--r--releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml7
-rw-r--r--releasenotes/notes/story-2007727-69b705c561309742.yaml6
-rw-r--r--requirements.txt4
-rw-r--r--tox.ini6
15 files changed, 495 insertions, 362 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index c04f3446..7dca539a 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,3 +1,4 @@
+---
- job:
name: osc-tox-unit-tips
parent: openstack-tox
@@ -220,12 +221,10 @@
- openstackclient-plugin-jobs
- osc-tox-unit-tips
- openstack-cover-jobs
- - openstack-lower-constraints-jobs
- openstack-python3-victoria-jobs
- publish-openstack-docs-pti
- check-requirements
- release-notes-jobs-python3
- - lib-forward-testing-python3
check:
jobs:
- osc-build-image
diff --git a/lower-constraints.txt b/lower-constraints.txt
deleted file mode 100644
index d880484b..00000000
--- a/lower-constraints.txt
+++ /dev/null
@@ -1,142 +0,0 @@
-amqp==2.1.1
-aodhclient==0.9.0
-appdirs==1.3.0
-asn1crypto==0.23.0
-bandit==1.1.0
-cachetools==2.0.0
-cffi==1.14.0
-cliff==2.8.0
-cmd2==0.8.0
-contextlib2==0.4.0
-coverage==4.0
-cryptography==2.7
-ddt==1.0.1
-debtcollector==1.2.0
-decorator==4.4.1
-deprecation==1.0
-docker-pycreds==0.2.1
-docker==2.4.2
-dogpile.cache==0.6.5
-eventlet==0.18.2
-extras==1.0.0
-fasteners==0.7.0
-fixtures==3.0.0
-flake8-import-order==0.13
-flake8==2.6.2
-future==0.16.0
-futurist==2.1.0
-gitdb==0.6.4
-GitPython==1.0.1
-gnocchiclient==3.3.1
-greenlet==0.4.15
-hacking==2.0.0
-httplib2==0.9.1
-idna==2.6
-iso8601==0.1.11
-Jinja2==2.10
-jmespath==0.9.0
-jsonpatch==1.16
-jsonpointer==1.13
-jsonschema==2.6.0
-keystoneauth1==3.18.0
-kombu==4.0.0
-linecache2==1.0.0
-MarkupSafe==1.1.0
-mccabe==0.2.1
-monotonic==0.6
-mox3==0.20.0
-msgpack-python==0.4.0
-munch==2.1.0
-netaddr==0.7.18
-netifaces==0.10.4
-openstacksdk==0.48.0
-os-service-types==1.7.0
-os-testr==1.0.0
-osc-lib==2.0.0
-osc-placement==1.7.0
-oslo.concurrency==3.26.0
-oslo.config==5.2.0
-oslo.context==2.19.2
-oslo.i18n==3.15.3
-oslo.log==3.36.0
-oslo.messaging==5.29.0
-oslo.middleware==3.31.0
-oslo.serialization==2.18.0
-oslo.service==1.24.0
-oslo.utils==3.33.0
-oslotest==3.2.0
-osprofiler==1.4.0
-paramiko==2.0.0
-Paste==2.0.2
-PasteDeploy==1.5.0
-pbr==2.0.0
-pika-pool==0.1.3
-pika==0.10.0
-ply==3.10
-positional==1.2.1
-prettytable==0.7.2
-pyasn1==0.1.8
-pycodestyle==2.0.0
-pycparser==2.18
-pyflakes==0.8.1
-pyinotify==0.9.6
-pyOpenSSL==17.1.0
-pyparsing==2.1.0
-pyperclip==1.5.27
-python-barbicanclient==4.5.2
-python-cinderclient==3.3.0
-python-dateutil==2.5.3
-python-designateclient==2.7.0
-python-glanceclient==2.8.0
-python-heatclient==1.10.0
-python-ironic-inspector-client==1.5.0
-python-ironicclient==2.3.0
-python-karborclient==0.6.0
-python-keystoneclient==3.22.0
-python-mimeparse==1.6.0
-python-mistralclient==3.1.0
-python-muranoclient==0.8.2
-python-neutronclient==6.7.0
-python-novaclient==15.1.0
-python-octaviaclient==1.11.0
-python-rsdclient==1.0.1
-python-saharaclient==1.4.0
-python-searchlightclient==1.0.0
-python-senlinclient==1.1.0
-python-subunit==1.0.0
-python-swiftclient==3.2.0
-python-troveclient==3.1.0
-python-watcherclient==2.5.0
-python-zaqarclient==1.0.0
-python-zunclient==3.6.0
-pytz==2013.6
-PyYAML==3.13
-repoze.lru==0.7
-requests-mock==1.2.0
-requests==2.14.2
-requestsexceptions==1.2.0
-rfc3986==0.3.1
-Routes==2.3.1
-rsd-lib==0.1.0
-simplejson==3.5.1
-six==1.10.0
-smmap==0.9.0
-statsd==3.2.1
-stestr==1.0.0
-stevedore==2.0.1
-sushy==0.1.0
-tempest==17.1.0
-tenacity==3.2.1
-testrepository==0.0.18
-testtools==2.2.0
-traceback2==1.4.0
-ujson==1.35
-unittest2==1.1.0
-urllib3==1.21.1
-validictory==1.1.1
-vine==1.1.4
-warlock==1.2.0
-WebOb==1.7.1
-websocket-client==0.44.0
-wrapt==1.7.0
-yaql==1.1.3
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 1d1fc741..f4d49a74 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -1337,15 +1337,19 @@ class ListServer(command.Lister):
# flavor name is given, map it to ID.
flavor_id = None
if parsed_args.flavor:
- flavor_id = utils.find_resource(compute_client.flavors,
- parsed_args.flavor).id
+ flavor_id = utils.find_resource(
+ compute_client.flavors,
+ parsed_args.flavor,
+ ).id
# Nova only supports list servers searching by image ID. So if a
# image name is given, map it to ID.
image_id = None
if parsed_args.image:
- image_id = image_client.find_image(parsed_args.image,
- ignore_missing=False).id
+ image_id = image_client.find_image(
+ parsed_args.image,
+ ignore_missing=False,
+ ).id
search_opts = {
'reservation_id': parsed_args.reservation_id,
@@ -1395,76 +1399,85 @@ class ListServer(command.Lister):
try:
timeutils.parse_isotime(search_opts['changes-since'])
except ValueError:
+ msg = _('Invalid changes-since value: %s')
raise exceptions.CommandError(
- _('Invalid changes-since value: %s') %
- search_opts['changes-since']
+ msg % search_opts['changes-since']
)
+ columns = (
+ 'id',
+ 'name',
+ 'status',
+ )
+ column_headers = (
+ 'ID',
+ 'Name',
+ 'Status',
+ )
+
if parsed_args.long:
- columns = (
- 'ID',
- 'Name',
- 'Status',
+ columns += (
'OS-EXT-STS:task_state',
'OS-EXT-STS:power_state',
- 'Networks',
- 'Image Name',
- 'Image ID',
- 'Flavor Name',
- 'Flavor ID',
- 'OS-EXT-AZ:availability_zone',
- 'OS-EXT-SRV-ATTR:host',
- 'Metadata',
)
- column_headers = (
- 'ID',
- 'Name',
- 'Status',
+ column_headers += (
'Task State',
'Power State',
- 'Networks',
+ )
+
+ columns += ('networks',)
+ column_headers += ('Networks',)
+
+ if parsed_args.long:
+ columns += (
+ 'image_name',
+ 'image_id',
+ )
+ column_headers += (
'Image Name',
'Image ID',
- 'Flavor Name',
- 'Flavor ID',
- 'Availability Zone',
- 'Host',
- 'Properties',
)
- mixed_case_fields = [
- 'OS-EXT-STS:task_state',
- 'OS-EXT-STS:power_state',
- 'OS-EXT-AZ:availability_zone',
- 'OS-EXT-SRV-ATTR:host',
- ]
else:
if parsed_args.no_name_lookup:
- columns = (
- 'ID',
- 'Name',
- 'Status',
- 'Networks',
- 'Image ID',
- 'Flavor ID',
- )
+ columns += ('image_id',)
else:
- columns = (
- 'ID',
- 'Name',
- 'Status',
- 'Networks',
- 'Image Name',
+ columns += ('image_name',)
+ column_headers += ('Image',)
+
+ # microversion 2.47 puts the embedded flavor into the server response
+ # body but omits the id, so if not present we just expose the original
+ # flavor name in the output
+ if compute_client.api_version >= api_versions.APIVersion('2.47'):
+ columns += ('flavor_name',)
+ column_headers += ('Flavor',)
+ else:
+ if parsed_args.long:
+ columns += (
+ 'flavor_name',
+ 'flavor_id',
+ )
+ column_headers += (
'Flavor Name',
+ 'Flavor ID',
)
- column_headers = (
- 'ID',
- 'Name',
- 'Status',
- 'Networks',
- 'Image',
- 'Flavor',
+ else:
+ if parsed_args.no_name_lookup:
+ columns += ('flavor_id',)
+ else:
+ columns += ('flavor_name',)
+ column_headers += ('Flavor',)
+
+ if parsed_args.long:
+ columns += (
+ 'OS-EXT-AZ:availability_zone',
+ 'OS-EXT-SRV-ATTR:host',
+ 'metadata',
+ )
+ column_headers += (
+ 'Availability Zone',
+ 'Host',
+ 'Properties',
)
- mixed_case_fields = []
marker_id = None
@@ -1476,25 +1489,29 @@ class ListServer(command.Lister):
if parsed_args.deleted:
marker_id = parsed_args.marker
else:
- marker_id = utils.find_resource(compute_client.servers,
- parsed_args.marker).id
+ marker_id = utils.find_resource(
+ compute_client.servers,
+ parsed_args.marker,
+ ).id
- data = compute_client.servers.list(search_opts=search_opts,
- marker=marker_id,
- limit=parsed_args.limit)
+ data = compute_client.servers.list(
+ search_opts=search_opts,
+ marker=marker_id,
+ limit=parsed_args.limit)
images = {}
flavors = {}
if data and not parsed_args.no_name_lookup:
- # Create a dict that maps image_id to image object.
- # Needed so that we can display the "Image Name" column.
- # "Image Name" is not crucial, so we swallow any exceptions.
- # The 'image' attribute can be an empty string if the server was
- # booted from a volume.
+ # create a dict that maps image_id to image object, which is used
+ # to display the "Image Name" column. Note that 'image.id' can be
+ # empty for BFV instances and 'image' can be missing entirely if
+ # there are infra failures
if parsed_args.name_lookup_one_by_one or image_id:
- for i_id in set(filter(lambda x: x is not None,
- (s.image.get('id') for s in data
- if s.image))):
+ for i_id in set(
+ s.image['id'] for s in data
+ if s.image and s.image.get('id')
+ ):
+ # "Image Name" is not crucial, so we swallow any exceptions
try:
images[i_id] = image_client.get_image(i_id)
except Exception:
@@ -1507,12 +1524,17 @@ class ListServer(command.Lister):
except Exception:
pass
- # Create a dict that maps flavor_id to flavor object.
- # Needed so that we can display the "Flavor Name" column.
- # "Flavor Name" is not crucial, so we swallow any exceptions.
+ # create a dict that maps flavor_id to flavor object, which is used
+ # to display the "Flavor Name" column. Note that 'flavor.id' is not
+ # present on microversion 2.47 or later and 'flavor' won't be
+ # present if there are infra failures
if parsed_args.name_lookup_one_by_one or flavor_id:
- for f_id in set(filter(lambda x: x is not None,
- (s.flavor.get('id') for s in data))):
+ for f_id in set(
+ s.flavor['id'] for s in data
+ if s.flavor and s.flavor.get('id')
+ ):
+ # "Flavor Name" is not crucial, so we swallow any
+ # exceptions
try:
flavors[f_id] = compute_client.flavors.get(f_id)
except Exception:
@@ -1536,6 +1558,7 @@ class ListServer(command.Lister):
# processing of the image and flavor informations.
if not hasattr(s, 'image') or not hasattr(s, 'flavor'):
continue
+
if 'id' in s.image:
image = images.get(s.image['id'])
if image:
@@ -1548,28 +1571,29 @@ class ListServer(command.Lister):
# 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:
+
+ if compute_client.api_version < api_versions.APIVersion('2.47'):
flavor = flavors.get(s.flavor['id'])
if flavor:
s.flavor_name = flavor.name
s.flavor_id = s.flavor['id']
else:
- # TODO(mriedem): Fix this for microversion >= 2.47 where the
- # flavor is embedded in the server response without the id.
- # We likely need to drop the Flavor ID column in that case if
- # --long is specified.
- s.flavor_name = ''
- s.flavor_id = ''
+ s.flavor_name = s.flavor['original_name']
table = (column_headers,
(utils.get_item_properties(
s, columns,
- mixed_case_fields=mixed_case_fields,
+ mixed_case_fields=(
+ 'OS-EXT-STS:task_state',
+ 'OS-EXT-STS:power_state',
+ 'OS-EXT-AZ:availability_zone',
+ 'OS-EXT-SRV-ATTR:host',
+ ),
formatters={
'OS-EXT-STS:power_state':
_format_servers_list_power_state,
- 'Networks': _format_servers_list_networks,
- 'Metadata': utils.format_dict,
+ 'networks': _format_servers_list_networks,
+ 'metadata': utils.format_dict,
},
) for s in data))
return table
diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py
index 1af6e28d..35f537db 100644
--- a/openstackclient/compute/v2/server_group.py
+++ b/openstackclient/compute/v2/server_group.py
@@ -56,12 +56,18 @@ class CreateServerGroup(command.ShowOne):
parser.add_argument(
'--policy',
metavar='<policy>',
+ choices=[
+ 'affinity',
+ 'anti-affinity',
+ 'soft-affinity',
+ 'soft-anti-affinity',
+ ],
default='affinity',
- help=_("Add a policy to <name> "
- "('affinity' or 'anti-affinity', "
- "defaults to 'affinity'). Specify --os-compute-api-version "
- "2.15 or higher for the 'soft-affinity' or "
- "'soft-anti-affinity' policy.")
+ help=_(
+ "Add a policy to <name> "
+ "Specify --os-compute-api-version 2.15 or higher for the "
+ "'soft-affinity' or 'soft-anti-affinity' policy."
+ )
)
return parser
@@ -69,9 +75,18 @@ class CreateServerGroup(command.ShowOne):
compute_client = self.app.client_manager.compute
info = {}
+ if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'):
+ if compute_client.api_version < api_versions.APIVersion('2.15'):
+ msg = _(
+ '--os-compute-api-version 2.15 or greater is required to '
+ 'support the %s policy'
+ )
+ raise exceptions.CommandError(msg % parsed_args.policy)
+
policy_arg = {'policies': [parsed_args.policy]}
if compute_client.api_version >= api_versions.APIVersion("2.64"):
policy_arg = {'policy': parsed_args.policy}
+
server_group = compute_client.server_groups.create(
name=parsed_args.name, **policy_arg)
@@ -135,11 +150,47 @@ class ListServerGroup(command.Lister):
default=False,
help=_("List additional fields in output")
)
+ # TODO(stephenfin): This should really be a --marker option, but alas
+ # the API doesn't support that for some reason
+ parser.add_argument(
+ '--offset',
+ metavar='<offset>',
+ type=int,
+ default=None,
+ help=_(
+ 'Index from which to start listing servers. This should '
+ 'typically be a factor of --limit. Display all servers groups '
+ 'if not specified.'
+ ),
+ )
+ parser.add_argument(
+ '--limit',
+ metavar='<limit>',
+ type=int,
+ default=None,
+ help=_(
+ "Maximum number of server groups to display. "
+ "If limit is greater than 'osapi_max_limit' option of Nova "
+ "API, 'osapi_max_limit' will be used instead."
+ ),
+ )
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
- data = compute_client.server_groups.list(parsed_args.all_projects)
+
+ kwargs = {}
+
+ if parsed_args.all_projects:
+ kwargs['all_projects'] = parsed_args.all_projects
+
+ if parsed_args.offset:
+ kwargs['offset'] = parsed_args.offset
+
+ if parsed_args.limit:
+ kwargs['limit'] = parsed_args.limit
+
+ data = compute_client.server_groups.list(**kwargs)
policy_key = 'Policies'
if compute_client.api_version >= api_versions.APIVersion("2.64"):
diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py
index cf1d6817..64aa3fcd 100644
--- a/openstackclient/image/v1/image.py
+++ b/openstackclient/image/v1/image.py
@@ -478,7 +478,11 @@ class SaveImage(command.Command):
image_client = self.app.client_manager.image
image = image_client.find_image(parsed_args.image)
- image_client.download_image(image.id, output=parsed_args.file)
+ output_file = parsed_args.file
+ if output_file is None:
+ output_file = getattr(sys.stdout, "buffer", sys.stdout)
+
+ image_client.download_image(image.id, stream=True, output=output_file)
class SetImage(command.Command):
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py
index 029f57a3..2836e764 100644
--- a/openstackclient/image/v2/image.py
+++ b/openstackclient/image/v2/image.py
@@ -803,7 +803,11 @@ class SaveImage(command.Command):
image_client = self.app.client_manager.image
image = image_client.find_image(parsed_args.image)
- image_client.download_image(image.id, output=parsed_args.file)
+ output_file = parsed_args.file
+ if output_file is None:
+ output_file = getattr(sys.stdout, "buffer", sys.stdout)
+
+ image_client.download_image(image.id, stream=True, output=output_file)
class SetImage(command.Command):
diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py
index 02bb406c..af589228 100644
--- a/openstackclient/tests/unit/compute/v2/test_server.py
+++ b/openstackclient/tests/unit/compute/v2/test_server.py
@@ -2557,7 +2557,7 @@ class TestServerDumpCreate(TestServer):
self.run_method_with_servers('trigger_crash_dump', 3)
-class TestServerList(TestServer):
+class _TestServerList(TestServer):
# Columns to be listed up.
columns = (
@@ -2585,7 +2585,7 @@ class TestServerList(TestServer):
)
def setUp(self):
- super(TestServerList, self).setUp()
+ super(_TestServerList, self).setUp()
self.search_opts = {
'reservation_id': None,
@@ -2643,10 +2643,11 @@ class TestServerList(TestServer):
# Get the command object to test
self.cmd = server.ListServer(self.app, None)
- # Prepare data returned by fake Nova API.
- self.data = []
- self.data_long = []
- self.data_no_name_lookup = []
+
+class TestServerList(_TestServerList):
+
+ def setUp(self):
+ super(TestServerList, self).setUp()
Image = collections.namedtuple('Image', 'id name')
self.images_mock.return_value = [
@@ -2661,8 +2662,8 @@ class TestServerList(TestServer):
for s in self.servers
]
- for s in self.servers:
- self.data.append((
+ self.data = tuple(
+ (
s.id,
s.name,
s.status,
@@ -2670,34 +2671,8 @@ class TestServerList(TestServer):
# Image will be an empty string if boot-from-volume
self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
self.flavor.name,
- ))
- self.data_long.append((
- s.id,
- s.name,
- s.status,
- getattr(s, 'OS-EXT-STS:task_state'),
- server._format_servers_list_power_state(
- getattr(s, 'OS-EXT-STS:power_state')
- ),
- server._format_servers_list_networks(s.networks),
- # Image will be an empty string if boot-from-volume
- 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'),
- getattr(s, 'OS-EXT-SRV-ATTR:host'),
- s.Metadata,
- ))
- self.data_no_name_lookup.append((
- s.id,
- s.name,
- 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 server.IMAGE_STRING_FOR_BFV,
- s.flavor['id']
- ))
+ ) for s in self.servers
+ )
def test_server_list_no_option(self):
arglist = []
@@ -2718,7 +2693,7 @@ class TestServerList(TestServer):
self.assertFalse(self.flavors_mock.get.call_count)
self.assertFalse(self.get_image_mock.call_count)
self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertEqual(self.data, tuple(data))
def test_server_list_no_servers(self):
arglist = []
@@ -2737,9 +2712,28 @@ class TestServerList(TestServer):
self.assertEqual(0, self.images_mock.list.call_count)
self.assertEqual(0, self.flavors_mock.list.call_count)
self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertEqual(self.data, tuple(data))
def test_server_list_long_option(self):
+ self.data = tuple(
+ (
+ s.id,
+ s.name,
+ s.status,
+ getattr(s, 'OS-EXT-STS:task_state'),
+ server._format_servers_list_power_state(
+ getattr(s, 'OS-EXT-STS:power_state')
+ ),
+ server._format_servers_list_networks(s.networks),
+ # Image will be an empty string if boot-from-volume
+ 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'),
+ getattr(s, 'OS-EXT-SRV-ATTR:host'),
+ s.Metadata,
+ ) for s in self.servers)
arglist = [
'--long',
]
@@ -2750,12 +2744,23 @@ class TestServerList(TestServer):
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
-
self.servers_mock.list.assert_called_with(**self.kwargs)
self.assertEqual(self.columns_long, columns)
- self.assertEqual(tuple(self.data_long), tuple(data))
+ self.assertEqual(self.data, tuple(data))
def test_server_list_no_name_lookup_option(self):
+ self.data = tuple(
+ (
+ s.id,
+ s.name,
+ 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 server.IMAGE_STRING_FOR_BFV,
+ s.flavor['id']
+ ) for s in self.servers
+ )
+
arglist = [
'--no-name-lookup',
]
@@ -2769,9 +2774,21 @@ class TestServerList(TestServer):
self.servers_mock.list.assert_called_with(**self.kwargs)
self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data_no_name_lookup), tuple(data))
+ self.assertEqual(self.data, tuple(data))
def test_server_list_n_option(self):
+ self.data = tuple(
+ (
+ s.id,
+ s.name,
+ 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 server.IMAGE_STRING_FOR_BFV,
+ s.flavor['id']
+ ) for s in self.servers
+ )
+
arglist = [
'-n',
]
@@ -2785,7 +2802,7 @@ class TestServerList(TestServer):
self.servers_mock.list.assert_called_with(**self.kwargs)
self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data_no_name_lookup), tuple(data))
+ self.assertEqual(self.data, tuple(data))
def test_server_list_name_lookup_one_by_one(self):
arglist = [
@@ -2807,7 +2824,7 @@ class TestServerList(TestServer):
self.flavors_mock.get.assert_called()
self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertEqual(self.data, tuple(data))
def test_server_list_with_image(self):
@@ -2828,145 +2845,213 @@ class TestServerList(TestServer):
self.servers_mock.list.assert_called_with(**self.kwargs)
self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertEqual(self.data, tuple(data))
- def test_server_list_with_locked_pre_v273(self):
+ def test_server_list_with_flavor(self):
arglist = [
- '--locked'
+ '--flavor', self.flavor.id
]
verifylist = [
- ('locked', True)
+ ('flavor', self.flavor.id)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
- ex = self.assertRaises(exceptions.CommandError,
- self.cmd.take_action,
- parsed_args)
- self.assertIn(
- '--os-compute-api-version 2.73 or greater is required', str(ex))
+ columns, data = self.cmd.take_action(parsed_args)
- def test_server_list_with_locked_v273(self):
+ self.flavors_mock.get.has_calls(self.flavor.id)
+
+ self.search_opts['flavor'] = self.flavor.id
+ self.servers_mock.list.assert_called_with(**self.kwargs)
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.data, tuple(data))
+
+ def test_server_list_with_changes_since(self):
- self.app.client_manager.compute.api_version = \
- api_versions.APIVersion('2.73')
arglist = [
- '--locked'
+ '--changes-since', '2016-03-04T06:27:59Z',
+ '--deleted'
]
verifylist = [
- ('locked', True)
+ ('changes_since', '2016-03-04T06:27:59Z'),
+ ('deleted', True),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.search_opts['locked'] = True
+ self.search_opts['changes-since'] = '2016-03-04T06: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))
+ self.assertEqual(self.data, tuple(data))
- def test_server_list_with_unlocked_v273(self):
+ @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError)
+ def test_server_list_with_invalid_changes_since(self, mock_parse_isotime):
- self.app.client_manager.compute.api_version = \
- api_versions.APIVersion('2.73')
arglist = [
- '--unlocked'
+ '--changes-since', 'Invalid time value',
]
verifylist = [
- ('unlocked', True)
+ ('changes_since', 'Invalid time value'),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
- columns, data = self.cmd.take_action(parsed_args)
+ try:
+ self.cmd.take_action(parsed_args)
+ self.fail('CommandError should be raised.')
+ except exceptions.CommandError as e:
+ self.assertEqual('Invalid changes-since value: Invalid time '
+ 'value', str(e))
+ mock_parse_isotime.assert_called_once_with(
+ 'Invalid time value'
+ )
- self.search_opts['locked'] = False
- self.servers_mock.list.assert_called_with(**self.kwargs)
- self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+class TestServerListV273(_TestServerList):
+
+ # Columns to be listed up.
+ columns = (
+ 'ID',
+ 'Name',
+ 'Status',
+ 'Networks',
+ 'Image',
+ 'Flavor',
+ )
+ columns_long = (
+ 'ID',
+ 'Name',
+ 'Status',
+ 'Task State',
+ 'Power State',
+ 'Networks',
+ 'Image Name',
+ 'Image ID',
+ 'Flavor',
+ 'Availability Zone',
+ 'Host',
+ 'Properties',
+ )
- def test_server_list_with_locked_and_unlocked_v273(self):
+ def setUp(self):
+ super(TestServerListV273, self).setUp()
+
+ # The fake servers' attributes. Use the original attributes names in
+ # nova, not the ones printed by "server list" command.
+ self.attrs['flavor'] = {
+ 'vcpus': self.flavor.vcpus,
+ 'ram': self.flavor.ram,
+ 'disk': self.flavor.disk,
+ 'ephemeral': self.flavor.ephemeral,
+ 'swap': self.flavor.swap,
+ 'original_name': self.flavor.name,
+ 'extra_specs': self.flavor.properties,
+ }
+
+ # The servers to be listed.
+ self.servers = self.setup_servers_mock(3)
+ self.servers_mock.list.return_value = self.servers
+
+ Image = collections.namedtuple('Image', 'id name')
+ self.images_mock.return_value = [
+ Image(id=s.image['id'], name=self.image.name)
+ # Image will be an empty string if boot-from-volume
+ for s in self.servers if s.image
+ ]
+
+ # The flavor information is embedded, so now reason for this to be
+ # called
+ self.flavors_mock.list = mock.NonCallableMock()
+
+ self.data = tuple(
+ (
+ s.id,
+ s.name,
+ 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 server.IMAGE_STRING_FOR_BFV,
+ self.flavor.name,
+ ) for s in self.servers)
+
+ def test_server_list_with_locked_pre_v273(self):
- self.app.client_manager.compute.api_version = \
- api_versions.APIVersion('2.73')
arglist = [
- '--locked',
- '--unlocked'
+ '--locked'
]
verifylist = [
- ('locked', True),
- ('unlocked', True)
+ ('locked', True)
]
- ex = self.assertRaises(
- utils.ParserException,
- self.check_parser, self.cmd, arglist, verifylist)
- self.assertIn('Argument parse failed', str(ex))
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.73 or greater is required', str(ex))
- def test_server_list_with_flavor(self):
+ def test_server_list_with_locked(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.73')
arglist = [
- '--flavor', self.flavor.id
+ '--locked'
]
verifylist = [
- ('flavor', self.flavor.id)
+ ('locked', True)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.flavors_mock.get.has_calls(self.flavor.id)
-
- self.search_opts['flavor'] = self.flavor.id
+ self.search_opts['locked'] = True
self.servers_mock.list.assert_called_with(**self.kwargs)
- self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertItemsEqual(self.columns, columns)
+ self.assertItemsEqual(self.data, tuple(data))
- def test_server_list_with_changes_since(self):
+ def test_server_list_with_unlocked_v273(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.73')
arglist = [
- '--changes-since', '2016-03-04T06:27:59Z',
- '--deleted'
+ '--unlocked'
]
verifylist = [
- ('changes_since', '2016-03-04T06:27:59Z'),
- ('deleted', True),
+ ('unlocked', True)
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
- self.search_opts['changes-since'] = '2016-03-04T06:27:59Z'
- self.search_opts['deleted'] = True
+ self.search_opts['locked'] = False
self.servers_mock.list.assert_called_with(**self.kwargs)
- self.assertEqual(self.columns, columns)
- self.assertEqual(tuple(self.data), tuple(data))
+ self.assertItemsEqual(self.columns, columns)
+ self.assertItemsEqual(self.data, tuple(data))
- @mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError)
- def test_server_list_with_invalid_changes_since(self, mock_parse_isotime):
+ def test_server_list_with_locked_and_unlocked(self):
+ self.app.client_manager.compute.api_version = \
+ api_versions.APIVersion('2.73')
arglist = [
- '--changes-since', 'Invalid time value',
+ '--locked',
+ '--unlocked'
]
verifylist = [
- ('changes_since', 'Invalid time value'),
+ ('locked', True),
+ ('unlocked', True)
]
- 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-since value: Invalid time '
- 'value', str(e))
- mock_parse_isotime.assert_called_once_with(
- 'Invalid time value'
- )
+ ex = self.assertRaises(
+ utils.ParserException,
+ self.check_parser, self.cmd, arglist, verifylist)
+ self.assertIn('Argument parse failed', str(ex))
- def test_server_list_v266_with_changes_before(self):
+ def test_server_list_with_changes_before(self):
self.app.client_manager.compute.api_version = (
api_versions.APIVersion('2.66'))
arglist = [
@@ -2983,13 +3068,14 @@ class TestServerList(TestServer):
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))
+ self.assertItemsEqual(self.columns, columns)
+ self.assertItemsEqual(self.data, tuple(data))
@mock.patch.object(timeutils, 'parse_isotime', side_effect=ValueError)
- def test_server_list_v266_with_invalid_changes_before(
+ def test_server_list_with_invalid_changes_before(
self, mock_parse_isotime):
self.app.client_manager.compute.api_version = (
api_versions.APIVersion('2.66'))
diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py
index 359cd2bd..50d2a57e 100644
--- a/openstackclient/tests/unit/compute/v2/test_server_group.py
+++ b/openstackclient/tests/unit/compute/v2/test_server_group.py
@@ -91,6 +91,28 @@ class TestServerGroupCreate(TestServerGroup):
def test_server_group_create(self):
arglist = [
+ '--policy', 'anti-affinity',
+ 'affinity_group',
+ ]
+ verifylist = [
+ ('policy', 'anti-affinity'),
+ ('name', 'affinity_group'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ columns, data = self.cmd.take_action(parsed_args)
+ self.server_groups_mock.create.assert_called_once_with(
+ name=parsed_args.name,
+ policies=[parsed_args.policy],
+ )
+
+ self.assertEqual(self.columns, columns)
+ self.assertEqual(self.data, data)
+
+ def test_server_group_create_with_soft_policies(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.15')
+
+ arglist = [
'--policy', 'soft-anti-affinity',
'affinity_group',
]
@@ -108,6 +130,27 @@ class TestServerGroupCreate(TestServerGroup):
self.assertEqual(self.columns, columns)
self.assertEqual(self.data, data)
+ def test_server_group_create_with_soft_policies_pre_v215(self):
+ self.app.client_manager.compute.api_version = api_versions.APIVersion(
+ '2.14')
+
+ arglist = [
+ '--policy', 'soft-anti-affinity',
+ 'affinity_group',
+ ]
+ verifylist = [
+ ('policy', 'soft-anti-affinity'),
+ ('name', 'affinity_group'),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ ex = self.assertRaises(
+ exceptions.CommandError,
+ self.cmd.take_action,
+ parsed_args)
+ self.assertIn(
+ '--os-compute-api-version 2.15 or greater is required',
+ str(ex))
+
def test_server_group_create_v264(self):
self.app.client_manager.compute.api_version = api_versions.APIVersion(
'2.64')
@@ -255,10 +298,13 @@ class TestServerGroupList(TestServerGroup):
verifylist = [
('all_projects', False),
('long', False),
+ ('limit', None),
+ ('offset', None),
]
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.server_groups_mock.list.assert_called_once_with()
self.assertEqual(self.list_columns, columns)
self.assertEqual(self.list_data, tuple(data))
@@ -271,14 +317,49 @@ class TestServerGroupList(TestServerGroup):
verifylist = [
('all_projects', True),
('long', True),
+ ('limit', None),
+ ('offset', None),
]
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.server_groups_mock.list.assert_called_once_with(
+ all_projects=True)
self.assertEqual(self.list_columns_long, columns)
self.assertEqual(self.list_data_long, tuple(data))
+ def test_server_group_list_with_limit(self):
+ arglist = [
+ '--limit', '1',
+ ]
+ verifylist = [
+ ('all_projects', False),
+ ('long', False),
+ ('limit', 1),
+ ('offset', None),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.cmd.take_action(parsed_args)
+
+ self.server_groups_mock.list.assert_called_once_with(limit=1)
+
+ def test_server_group_list_with_offset(self):
+ arglist = [
+ '--offset', '5',
+ ]
+ verifylist = [
+ ('all_projects', False),
+ ('long', False),
+ ('limit', None),
+ ('offset', 5),
+ ]
+
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+ self.cmd.take_action(parsed_args)
+
+ self.server_groups_mock.list.assert_called_once_with(offset=5)
+
class TestServerGroupListV264(TestServerGroupV264):
@@ -328,7 +409,7 @@ class TestServerGroupListV264(TestServerGroupV264):
]
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.server_groups_mock.list.assert_called_once_with()
self.assertEqual(self.list_columns, columns)
self.assertEqual(self.list_data, tuple(data))
@@ -344,7 +425,8 @@ class TestServerGroupListV264(TestServerGroupV264):
]
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.server_groups_mock.list.assert_called_once_with(
+ all_projects=True)
self.assertEqual(self.list_columns_long, columns)
self.assertEqual(self.list_data_long, tuple(data))
diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py
index 310f6b76..a7f60398 100644
--- a/openstackclient/tests/unit/image/v2/test_image.py
+++ b/openstackclient/tests/unit/image/v2/test_image.py
@@ -1614,7 +1614,7 @@ class TestImageSave(TestImage):
verifylist = [
('file', '/path/to/file'),
- ('image', self.image.id)
+ ('image', self.image.id),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -1622,6 +1622,7 @@ class TestImageSave(TestImage):
self.client.download_image.assert_called_once_with(
self.image.id,
+ stream=True,
output='/path/to/file')
diff --git a/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml b/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml
new file mode 100644
index 00000000..b359e77c
--- /dev/null
+++ b/releasenotes/notes/add-missing-server-group-list-opts-d3c3d98b7f7a56a6.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Add ``--limit`` and ``--offset`` options to ``server group list`` command,
+ to configure pagination of results.
diff --git a/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml b/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml
new file mode 100644
index 00000000..fdb37bbb
--- /dev/null
+++ b/releasenotes/notes/fix-flavor-in-server-list-microversion-2.47-af200e9bb4747e2d.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Add support for compute API microversion 2.47, which changes how flavor
+ details are included in server detail responses. In 2.46 and below,
+ only the flavor ID was shown in the server detail response. Starting in
+ 2.47, flavor information is embedded in the server response. The newer
+ behavior is now supported.
diff --git a/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml b/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml
new file mode 100644
index 00000000..857f3c50
--- /dev/null
+++ b/releasenotes/notes/fix-openstak-image-save-sdk-port-eb160e8ffc92e514.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - Stream image download to avoid buffering data in memory which rapidly
+ exhausts memory resulting in OOM kill or system crash for all but the
+ smallest of images. Fixes https://storyboard.openstack.org/#!/story/2007672
+ - Restore default behavior of 'openstack image save' to send data to stdout
+ Relates to https://storyboard.openstack.org/#!/story/2007672. \ No newline at end of file
diff --git a/releasenotes/notes/story-2007727-69b705c561309742.yaml b/releasenotes/notes/story-2007727-69b705c561309742.yaml
new file mode 100644
index 00000000..4a9eeae3
--- /dev/null
+++ b/releasenotes/notes/story-2007727-69b705c561309742.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ The ``openstack server group create`` command will now validate the policy
+ value requested with ``--policy``, restricting it to the valid values
+ allowed by given microversions.
diff --git a/requirements.txt b/requirements.txt
index 2b7976e5..b8fa148f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,7 @@
+# Requirements lower bounds listed here are our best effort to keep them up to
+# date but we do not test them so no guarantee of having them all correct. If
+# you find any incorrect lower bounds, let us know or propose a fix.
+
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
diff --git a/tox.ini b/tox.ini
index bace7a4f..59577c44 100644
--- a/tox.ini
+++ b/tox.ini
@@ -138,9 +138,3 @@ exclude = .git,.tox,dist,doc,*lib/python*,*egg,build,tools
ignore = W504
import-order-style = pep8
application_import_names = openstackclient
-
-[testenv:lower-constraints]
-deps =
- -c{toxinidir}/lower-constraints.txt
- -r{toxinidir}/test-requirements.txt
- -r{toxinidir}/requirements.txt